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(); + } + } +}