From 07c84b39bab0269fc6e72a32a03da6641cbfa244 Mon Sep 17 00:00:00 2001 From: Ruwen Schwedewsky Date: Tue, 27 Feb 2018 13:40:15 +1100 Subject: [PATCH] Listen on an unencrypted and on an encrypted port Currently the SMPP server can listen on one port. This means that you can either listen with SSL or without. This PR allows the user to listen on an unencrypted and on an encrypted port. The change has been done in a way that keeps backwards compatibility. --- SSL.md | 25 ++- .../smpp/SmppServerConfiguration.java | 16 ++ .../smpp/channel/SmppServerConnector.java | 30 ++-- .../smpp/impl/DefaultSmppServer.java | 19 +- .../smpp/demo/TwoPortsServerMain.java | 145 +++++++++++++++ .../cloudhopper/smpp/ssl/TwoPortsTest.java | 168 ++++++++++++++++++ 6 files changed, 388 insertions(+), 15 deletions(-) create mode 100644 src/test/java/com/cloudhopper/smpp/demo/TwoPortsServerMain.java create mode 100644 src/test/java/com/cloudhopper/smpp/ssl/TwoPortsTest.java diff --git a/SSL.md b/SSL.md index 84d0ec4d..5b913a73 100644 --- a/SSL.md +++ b/SSL.md @@ -4,7 +4,9 @@ The purpose of this document is to provide a summary of how to configuration SSL ## Configuring a SMPP server with SSL transport -### Example: +There are two ways to use SSL on the server side: Either SSL only or non-SSL and SSL on separate ports (added in version 5.0.x). + +### Example for SSL only: // Configure the server as you normally would: SmppServerConfiguration configuration = new SmppServerConfiguration(); @@ -24,6 +26,27 @@ The purpose of this document is to provide a summary of how to configuration SSL configuration.setUseSsl(true); configuration.setSslConfiguration(sslConfig); +### Example for non-SSL and SSL: + + // Configure the server as you normally would: + SmppServerConfiguration configuration = new SmppServerConfiguration(); + configuration.setPort(2776); // 2776 serves unencrypted traffic + ... + + // Then create a SSL configuration: + SslConfiguration sslConfig = new SslConfiguration(); + sslConfig.setKeyStorePath("path/to/keystore"); + sslConfig.setKeyStorePassword("changeit"); + sslConfig.setKeyManagerPassword("changeit"); + sslConfig.setTrustStorePath("path/to/keystore"); + sslConfig.setTrustStorePassword("changeit"); + ... + + // And add it to the server configuration: + configuration.setUseSsl(true); + configuration.setSslConfiguration(sslConfig); + configuration.setSslPort(2777); // 2777 serves SSL-encrypted traffic + ### Require client auth diff --git a/src/main/java/com/cloudhopper/smpp/SmppServerConfiguration.java b/src/main/java/com/cloudhopper/smpp/SmppServerConfiguration.java index 6b3c43b5..759cc2ea 100644 --- a/src/main/java/com/cloudhopper/smpp/SmppServerConfiguration.java +++ b/src/main/java/com/cloudhopper/smpp/SmppServerConfiguration.java @@ -58,6 +58,7 @@ public class SmppServerConfiguration extends SmppConnectionConfiguration { private long defaultRequestExpiryTimeout = SmppConstants.DEFAULT_REQUEST_EXPIRY_TIMEOUT; private long defaultWindowMonitorInterval = SmppConstants.DEFAULT_WINDOW_MONITOR_INTERVAL; private boolean defaultSessionCountersEnabled = false; + private Integer sslPort; public SmppServerConfiguration() { super("0.0.0.0", 2775, 5000l); @@ -250,4 +251,19 @@ public void setDefaultSessionCountersEnabled(boolean defaultSessionCountersEnabl this.defaultSessionCountersEnabled = defaultSessionCountersEnabled; } + /** + * @return the SSL Port, might be null + */ + public Integer getSslPort() { + return sslPort; + } + + /** + * Sets the SSL port. If you just set the normal port and use SSL, then the server only supports SSL on the normal port. If you specify the SSL + * port, then the server will listen to unencrypted connections on the normal and the SSL connections on the SSL port. + * @param sslPort + */ + public void setSslPort(Integer sslPort) { + this.sslPort = sslPort; + } } diff --git a/src/main/java/com/cloudhopper/smpp/channel/SmppServerConnector.java b/src/main/java/com/cloudhopper/smpp/channel/SmppServerConnector.java index bf55300e..3c26c3dc 100644 --- a/src/main/java/com/cloudhopper/smpp/channel/SmppServerConnector.java +++ b/src/main/java/com/cloudhopper/smpp/channel/SmppServerConnector.java @@ -21,6 +21,7 @@ */ +import com.cloudhopper.smpp.SmppServerConfiguration; import com.cloudhopper.smpp.impl.DefaultSmppServer; import com.cloudhopper.smpp.impl.UnboundSmppSession; import com.cloudhopper.smpp.ssl.SslConfiguration; @@ -36,6 +37,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.net.InetAddress; +import java.net.InetSocketAddress; + /** * Channel handler for server SMPP sessions. * @@ -66,23 +70,27 @@ public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) thr // create a default "unbound" thread name for the thread processing the channel // this will create a name of "RemoteIPAddress.RemotePort" String channelName = ChannelUtil.createChannelName(channel); - String threadName = server.getConfiguration().getName() + ".UnboundSession." + channelName; + SmppServerConfiguration serverConfig = server.getConfiguration(); + String threadName = serverConfig.getName() + ".UnboundSession." + channelName; // rename the current thread for logging, then rename it back String currentThreadName = Thread.currentThread().getName(); - Thread.currentThread().setName(server.getConfiguration().getName()); + Thread.currentThread().setName(serverConfig.getName()); logger.info("New channel from [{}]", channelName); Thread.currentThread().setName(currentThreadName); - // add SSL handler - if (server.getConfiguration().isUseSsl()) { - SslConfiguration sslConfig = server.getConfiguration().getSslConfiguration(); - if (sslConfig == null) throw new IllegalStateException("sslConfiguration must be set"); - SslContextFactory factory = new SslContextFactory(sslConfig); - SSLEngine sslEngine = factory.newSslEngine(); - sslEngine.setUseClientMode(false); - channel.getPipeline().addLast(SmppChannelConstants.PIPELINE_SESSION_SSL_NAME, new SslHandler(sslEngine)); - } + // add SSL handler + if (serverConfig.isUseSsl() + && ((serverConfig.getSslPort() != null + && channel.getLocalAddress() instanceof InetSocketAddress + && ((InetSocketAddress)channel.getLocalAddress()).getPort() == serverConfig.getSslPort()) || serverConfig.getSslPort() == null)) { + SslConfiguration sslConfig = serverConfig.getSslConfiguration(); + if (sslConfig == null) throw new IllegalStateException("sslConfiguration must be set"); + SslContextFactory factory = new SslContextFactory(sslConfig); + SSLEngine sslEngine = factory.newSslEngine(); + sslEngine.setUseClientMode(false); + channel.getPipeline().addLast(SmppChannelConstants.PIPELINE_SESSION_SSL_NAME, new SslHandler(sslEngine)); + } // add a new instance of a thread renamer channel.getPipeline().addLast(SmppChannelConstants.PIPELINE_SESSION_THREAD_RENAMER_NAME, new SmppSessionThreadRenamer(threadName)); diff --git a/src/main/java/com/cloudhopper/smpp/impl/DefaultSmppServer.java b/src/main/java/com/cloudhopper/smpp/impl/DefaultSmppServer.java index 42ee583d..1caafd5b 100644 --- a/src/main/java/com/cloudhopper/smpp/impl/DefaultSmppServer.java +++ b/src/main/java/com/cloudhopper/smpp/impl/DefaultSmppServer.java @@ -78,7 +78,7 @@ public class DefaultSmppServer implements SmppServer, DefaultSmppServerMXBean { private ExecutorService bossThreadPool; private ChannelFactory channelFactory; private ServerBootstrap serverBootstrap; - private Channel serverChannel; + private ChannelGroup serverChannel; // shared instance of a timer for session writeTimeout timing private final org.jboss.netty.util.Timer writeTimeoutTimer; // shared instance of a timer background thread to close unbound channels @@ -223,7 +223,14 @@ public Timer getBindTimer() { @Override public boolean isStarted() { - return (this.serverChannel != null && this.serverChannel.isBound()); + if (serverChannel == null) { + return false; + } + boolean allStarted = true; + for(Channel channel : serverChannel) { + allStarted = allStarted && channel.isBound(); + } + return allStarted; } @Override @@ -242,8 +249,14 @@ public void start() throws SmppChannelException { throw new SmppChannelException("Unable to start: server is destroyed"); } try { - serverChannel = this.serverBootstrap.bind(new InetSocketAddress(configuration.getHost(), configuration.getPort())); + serverChannel = new DefaultChannelGroup(); + serverChannel.add(this.serverBootstrap.bind(new InetSocketAddress(configuration.getHost(), configuration.getPort()))); logger.info("{} started at {}:{}", configuration.getName(), configuration.getHost(), configuration.getPort()); + if (configuration.isUseSsl() && configuration.getSslPort() != null) { + serverChannel.add(this.serverBootstrap.bind(new InetSocketAddress(configuration.getHost(), configuration.getSslPort()))); + logger.info("{} started at {}:{} (SSL)", configuration.getName(), configuration.getHost(), configuration.getSslPort()); + } + } catch (ChannelException e) { throw new SmppChannelException(e.getMessage(), e); } diff --git a/src/test/java/com/cloudhopper/smpp/demo/TwoPortsServerMain.java b/src/test/java/com/cloudhopper/smpp/demo/TwoPortsServerMain.java new file mode 100644 index 00000000..e5d0394d --- /dev/null +++ b/src/test/java/com/cloudhopper/smpp/demo/TwoPortsServerMain.java @@ -0,0 +1,145 @@ +package com.cloudhopper.smpp.demo; + +/* + * #%L + * 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. + * #L% + */ + +import com.cloudhopper.smpp.*; +import com.cloudhopper.smpp.impl.DefaultSmppServer; +import com.cloudhopper.smpp.impl.DefaultSmppSessionHandler; +import com.cloudhopper.smpp.pdu.BaseBind; +import com.cloudhopper.smpp.pdu.BaseBindResp; +import com.cloudhopper.smpp.pdu.PduRequest; +import com.cloudhopper.smpp.pdu.PduResponse; +import com.cloudhopper.smpp.ssl.SslConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Demonstration of a simple SMPP server listening on a two ports: one unencrypted and one SSL-encrypted + * + * @author ruwen + */ +public class TwoPortsServerMain { + private static final Logger LOGGER = LoggerFactory.getLogger(TwoPortsServerMain.class); + + static public void main(String[] args) throws Exception { + // + // setup 3 things required for a server + // + + // for monitoring thread use, it's preferable to create your own instance + // of an executor and cast it to a ThreadPoolExecutor from Executors.newCachedThreadPool() + // this permits exposing things like executor.getActiveCount() via JMX possible + // no point renaming the threads in a factory since underlying Netty + // framework does not easily allow you to customize your thread names + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newCachedThreadPool(); + + // to enable automatic expiration of requests, a second scheduled executor + // is required which is what a monitor task will be executed with - this + // is probably a thread pool that can be shared with between all client bootstraps + ScheduledThreadPoolExecutor monitorExecutor = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(1, new ThreadFactory() { + private AtomicInteger sequence = new AtomicInteger(0); + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setName("SmppServerSessionWindowMonitorPool-" + sequence.getAndIncrement()); + return t; + } + }); + + // create a server configuration + SmppServerConfiguration configuration = new SmppServerConfiguration(); + configuration.setPort(2776); + configuration.setMaxConnectionSize(10); + configuration.setNonBlockingSocketsEnabled(true); + configuration.setDefaultRequestExpiryTimeout(30000); + configuration.setDefaultWindowMonitorInterval(15000); + configuration.setDefaultWindowSize(5); + configuration.setDefaultWindowWaitTimeout(configuration.getDefaultRequestExpiryTimeout()); + configuration.setDefaultSessionCountersEnabled(true); + configuration.setJmxEnabled(true); + + //ssl + SslConfiguration sslConfig = new SslConfiguration(); + sslConfig.setKeyStorePath("src/test/resources/keystore"); + sslConfig.setKeyStorePassword("changeit"); + sslConfig.setKeyManagerPassword("changeit"); + sslConfig.setTrustStorePath("src/test/resources/keystore"); + sslConfig.setTrustStorePassword("changeit"); + configuration.setUseSsl(true); + configuration.setSslConfiguration(sslConfig); + configuration.setSslPort(2777); + + // create a server, start it up + DefaultSmppServer smppServer = new DefaultSmppServer(configuration, new DefaultSmppServerHandler(), executor, monitorExecutor); + + LOGGER.info("Starting SMPP server..."); + smppServer.start(); + LOGGER.info("SMPP server started"); + + System.out.println("Press any key to stop server"); + System.in.read(); + + LOGGER.info("Stopping SMPP server..."); + smppServer.stop(); + LOGGER.info("SMPP server stopped"); + + LOGGER.info("Server counters: {}", smppServer.getCounters()); + } + + public static class DefaultSmppServerHandler implements SmppServerHandler { + + @Override + public void sessionBindRequested(Long sessionId, SmppSessionConfiguration sessionConfiguration, final BaseBind bindRequest) { + // test name change of sessions + // this name actually shows up as thread context.... + sessionConfiguration.setName("Application.SMPP." + sessionConfiguration.getSystemId()); + } + + @Override + public void sessionCreated(Long sessionId, SmppServerSession session, BaseBindResp preparedBindResponse) { + LOGGER.info("Session created: {}", session); + // need to do something it now (flag we're ready) + session.serverReady(new TestSmppSessionHandler()); + } + + @Override + public void sessionDestroyed(Long sessionId, SmppServerSession session) { + LOGGER.info("Session destroyed: {}", session); + // print out final stats + if (session.hasCounters()) { + LOGGER.info(" final session rx-submitSM: {}", session.getCounters().getRxSubmitSM()); + } + + // make sure it's really shutdown + session.destroy(); + } + } + + public static class TestSmppSessionHandler extends DefaultSmppSessionHandler { + @Override + public PduResponse firePduRequestReceived(PduRequest pduRequest) { + return pduRequest.createResponse(); + } + } +} diff --git a/src/test/java/com/cloudhopper/smpp/ssl/TwoPortsTest.java b/src/test/java/com/cloudhopper/smpp/ssl/TwoPortsTest.java new file mode 100644 index 00000000..f2e16703 --- /dev/null +++ b/src/test/java/com/cloudhopper/smpp/ssl/TwoPortsTest.java @@ -0,0 +1,168 @@ + +package com.cloudhopper.smpp.ssl; + +/* + * #%L + * 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. + * #L% + */ + +import com.cloudhopper.smpp.*; +import com.cloudhopper.smpp.impl.DefaultSmppClient; +import com.cloudhopper.smpp.impl.DefaultSmppServer; +import com.cloudhopper.smpp.impl.DefaultSmppSession; +import com.cloudhopper.smpp.impl.DefaultSmppSessionHandler; +import com.cloudhopper.smpp.pdu.*; +import com.cloudhopper.smpp.type.SmppProcessingException; +import org.jboss.netty.channel.Channel; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.net.InetSocketAddress; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; + +/** + * @author ruwen + */ +public class TwoPortsTest { + + private static final int PORT_UNENCRYPTED = 9784; + private static final int PORT_ENCRYPTED = 9785; + private static final String SYSTEMID = "smppclient1"; + private static final String PASSWORD = "password"; + + private TestSmppServerHandler serverHandler; + + @Before + public void setUp() { + serverHandler = new TestSmppServerHandler(); + } + + + private SmppSessionConfiguration createClientConfigurationNoSSL() { + SmppSessionConfiguration configuration = new SmppSessionConfiguration(); + configuration.setWindowSize(1); + configuration.setName("Tester.Session.0"); + configuration.setType(SmppBindType.TRANSCEIVER); + configuration.setHost("localhost"); + configuration.setPort(PORT_UNENCRYPTED); + configuration.setConnectTimeout(200); + configuration.setBindTimeout(200); + configuration.setSystemId(SYSTEMID); + configuration.setPassword(PASSWORD); + configuration.getLoggingOptions().setLogBytes(true); + return configuration; + } + + private SmppSessionConfiguration createClientConfigurationSSL() { + SmppSessionConfiguration configuration = createClientConfigurationNoSSL(); + SslConfiguration sslConfig = new SslConfiguration(); + configuration.setUseSsl(true); + configuration.setSslConfiguration(sslConfig); + configuration.setPort(PORT_ENCRYPTED); + return configuration; + } + + + private static class TestSmppServerHandler implements SmppServerHandler { + private Set sessions = new HashSet<>(); + private final Map portConnectionCounter = new HashMap<>(); + + private TestSmppServerHandler() { + portConnectionCounter.put(PORT_ENCRYPTED, new AtomicInteger(0)); + portConnectionCounter.put(PORT_UNENCRYPTED, new AtomicInteger(0)); + } + + @Override + public void sessionBindRequested(Long sessionId, SmppSessionConfiguration sessionConfiguration, final BaseBind bindRequest) throws + SmppProcessingException { + if (!SYSTEMID.equals(bindRequest.getSystemId())) { + throw new SmppProcessingException(SmppConstants.STATUS_INVSYSID); + } + if (!PASSWORD.equals(bindRequest.getPassword())) { + throw new SmppProcessingException(SmppConstants.STATUS_INVPASWD); + } + } + + @Override + public void sessionCreated(Long sessionId, SmppServerSession session, BaseBindResp preparedBindResponse) { + sessions.add(session); + Channel channel = ((DefaultSmppSession) session).getChannel(); + portConnectionCounter.get(((InetSocketAddress)channel.getLocalAddress()).getPort()).incrementAndGet(); + session.serverReady(new TestSmppSessionHandler()); + } + + @Override + public void sessionDestroyed(Long sessionId, SmppServerSession session) { + sessions.remove(session); + } + } + + public static class TestSmppSessionHandler extends DefaultSmppSessionHandler { + @Override + public PduResponse firePduRequestReceived(PduRequest pduRequest) { + return pduRequest.createResponse(); + } + } + + @Test + public void connectViaTwoPorts() throws Exception { + SslConfiguration sslConfig = new SslConfiguration(); + sslConfig.setKeyStorePath("src/test/resources/keystore"); + sslConfig.setKeyStorePassword("changeit"); + sslConfig.setKeyManagerPassword("changeit"); + sslConfig.setTrustStorePath("src/test/resources/keystore"); + sslConfig.setTrustStorePassword("changeit"); + + SmppServerConfiguration configuration = new SmppServerConfiguration(); + configuration.setPort(PORT_UNENCRYPTED); + configuration.setSslPort(PORT_ENCRYPTED); + configuration.setSystemId("cloudhopper"); + configuration.setUseSsl(true); + configuration.setSslConfiguration(sslConfig); + + + DefaultSmppServer server = new DefaultSmppServer(configuration, serverHandler); + try { + server.start(); + + DefaultSmppClient clientNoSsl = new DefaultSmppClient(); + DefaultSmppClient clientSsl = new DefaultSmppClient(); + + // this should actually work + SmppSession clientNoSslSession = clientNoSsl.bind(createClientConfigurationNoSSL()); + SmppSession clientSslSession = clientSsl.bind(createClientConfigurationSSL()); + + Thread.sleep(200); + assertEquals(2, serverHandler.portConnectionCounter.size()); + assertEquals(1, serverHandler.portConnectionCounter.get(PORT_ENCRYPTED).get()); + assertEquals(1, serverHandler.portConnectionCounter.get(PORT_UNENCRYPTED).get()); + assertEquals(2, serverHandler.sessions.size()); + + clientNoSslSession.close(); + clientSslSession.close(); + + Thread.sleep(200); + assertEquals(0, serverHandler.sessions.size()); + } finally { + server.destroy(); + } + } +}