From 96f739ef83db99181de60e4b7e35f987dd436793 Mon Sep 17 00:00:00 2001 From: Eric Long Date: Wed, 23 Oct 2019 13:47:25 -0400 Subject: [PATCH] 0004119: Security token in header and session authentication --- .../symmetric/common/ParameterConstants.java | 5 +- .../impl/AbstractOfflineDetectorService.java | 24 +- .../service/impl/BandwidthService.java | 26 +-- .../transport/AuthenticationException.java | 13 ++ .../transport/http/HttpIncomingTransport.java | 70 +++--- .../transport/http/HttpOutgoingTransport.java | 81 ++++--- .../transport/http/HttpTransportManager.java | 207 +++++++++--------- .../jumpmind/symmetric/web/WebConstants.java | 6 +- .../resources/symmetric-default.properties | 27 +++ .../jumpmind/symmetric/SymmetricLauncher.java | 16 -- .../symmetric/SymmetricWebServer.java | 48 +--- .../web/AuthenticationInterceptor.java | 57 ++++- .../symmetric/web/ServerSymmetricEngine.java | 4 +- 13 files changed, 302 insertions(+), 282 deletions(-) diff --git a/symmetric-core/src/main/java/org/jumpmind/symmetric/common/ParameterConstants.java b/symmetric-core/src/main/java/org/jumpmind/symmetric/common/ParameterConstants.java index ec10871699..a45206fd0f 100644 --- a/symmetric-core/src/main/java/org/jumpmind/symmetric/common/ParameterConstants.java +++ b/symmetric-core/src/main/java/org/jumpmind/symmetric/common/ParameterConstants.java @@ -271,8 +271,9 @@ private ParameterConstants() { public final static String TRANSPORT_HTTP_COMPRESSION_DISABLED_SERVLET = "web.compression.disabled"; public final static String TRANSPORT_HTTP_COMPRESSION_LEVEL = "compression.level"; public final static String TRANSPORT_HTTP_COMPRESSION_STRATEGY = "compression.strategy"; - public final static String TRANSPORT_HTTP_BASIC_AUTH_USERNAME = "http.basic.auth.username"; - public final static String TRANSPORT_HTTP_BASIC_AUTH_PASSWORD = "http.basic.auth.password"; + public final static String TRANSPORT_HTTP_USE_SESSION_AUTH = "http.use.session.auth"; + public final static String TRANSPORT_HTTP_SESSION_EXPIRE_SECONDS = "http.session.expire.seconds"; + public final static String TRANSPORT_HTTP_USE_HEADER_SECURITY_TOKEN = "http.use.header.security.token"; public final static String TRANSPORT_TYPE = "transport.type"; public final static String TRANSPORT_MAX_BYTES_TO_SYNC = "transport.max.bytes.to.sync"; public final static String TRANSPORT_MAX_ERROR_MILLIS = "transport.max.error.millis"; diff --git a/symmetric-core/src/main/java/org/jumpmind/symmetric/service/impl/AbstractOfflineDetectorService.java b/symmetric-core/src/main/java/org/jumpmind/symmetric/service/impl/AbstractOfflineDetectorService.java index b7226d2343..ed13bb88df 100644 --- a/symmetric-core/src/main/java/org/jumpmind/symmetric/service/impl/AbstractOfflineDetectorService.java +++ b/symmetric-core/src/main/java/org/jumpmind/symmetric/service/impl/AbstractOfflineDetectorService.java @@ -101,7 +101,11 @@ protected void fireOffline(Exception exception, Node remoteNode, RemoteNodeStatu } status.setStatus(Status.BUSY); } else if (isNotAuthenticated(exception)) { - log.warn("Authorization denied from {} at {}", new Object[] {remoteNode, syncUrl}); + if (isAuthenticationExpired(exception)) { + log.debug("Authentication is required again to renew session"); + } else { + log.warn("Authorization denied from {} at {}", new Object[] {remoteNode, syncUrl}); + } status.setStatus(Status.NOT_AUTHORIZED); } else if (isSyncDisabled(exception)) { log.warn("Sync was not enabled for {} at {}", new Object[] {remoteNode, syncUrl}); @@ -189,6 +193,24 @@ protected boolean isNotAuthenticated(Exception ex) { return offline; } + protected boolean isAuthenticationExpired(Exception ex) { + boolean expired = false; + if (ex != null) { + Throwable cause = getRootCause(ex); + AuthenticationException authException = null; + if (ex instanceof AuthenticationException) { + authException = (AuthenticationException) ex; + } + if (cause instanceof AuthenticationException) { + authException = (AuthenticationException) cause; + } + if (authException != null) { + expired = authException.isExpiredSession(); + } + } + return expired; + } + protected boolean isBusy(Exception ex) { boolean offline = false; if (ex != null) { diff --git a/symmetric-core/src/main/java/org/jumpmind/symmetric/service/impl/BandwidthService.java b/symmetric-core/src/main/java/org/jumpmind/symmetric/service/impl/BandwidthService.java index 12dfc46eb2..9b9c6e1a43 100644 --- a/symmetric-core/src/main/java/org/jumpmind/symmetric/service/impl/BandwidthService.java +++ b/symmetric-core/src/main/java/org/jumpmind/symmetric/service/impl/BandwidthService.java @@ -26,11 +26,9 @@ import java.net.SocketTimeoutException; import java.net.URL; -import org.jumpmind.symmetric.common.ParameterConstants; import org.jumpmind.symmetric.service.IBandwidthService; import org.jumpmind.symmetric.service.IParameterService; import org.jumpmind.symmetric.transport.BandwidthTestResults; -import org.jumpmind.symmetric.transport.http.HttpTransportManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,10 +39,7 @@ public class BandwidthService implements IBandwidthService { protected final Logger log = LoggerFactory.getLogger(getClass()); - private IParameterService parameterService; - public BandwidthService(IParameterService parameterService) { - this.parameterService = parameterService; } public double getDownloadKbpsFor(String syncUrl, long sampleSize, long maxTestDuration) { @@ -70,7 +65,6 @@ protected BandwidthTestResults getDownloadResultsFor(String syncUrl, long sample URL u = new URL(String.format("%s/bandwidth?sampleSize=%s", syncUrl, sampleSize)); bw.start(); HttpURLConnection conn = (HttpURLConnection) u.openConnection(); - setBasicAuthIfNeeded(conn); conn.connect(); is = conn.getInputStream(); @@ -83,20 +77,12 @@ protected BandwidthTestResults getDownloadResultsFor(String syncUrl, long sample log.info("{} was calculated to have a download bandwidth of {} kbps", syncUrl, bw.getKbps()); return bw; } finally { - if(is != null) { - try { - is.close(); - } catch(IOException e) { } - } - } - } - - protected void setBasicAuthIfNeeded(HttpURLConnection conn) { - if (parameterService != null) { - HttpTransportManager.setBasicAuthIfNeeded(conn, parameterService - .getString(ParameterConstants.TRANSPORT_HTTP_BASIC_AUTH_USERNAME), - parameterService - .getString(ParameterConstants.TRANSPORT_HTTP_BASIC_AUTH_PASSWORD)); + if (is != null) { + try { + is.close(); + } catch (IOException e) { + } + } } } diff --git a/symmetric-core/src/main/java/org/jumpmind/symmetric/transport/AuthenticationException.java b/symmetric-core/src/main/java/org/jumpmind/symmetric/transport/AuthenticationException.java index bcbc9f3832..0cb9ae0f54 100644 --- a/symmetric-core/src/main/java/org/jumpmind/symmetric/transport/AuthenticationException.java +++ b/symmetric-core/src/main/java/org/jumpmind/symmetric/transport/AuthenticationException.java @@ -25,4 +25,17 @@ public class AuthenticationException extends OfflineException { private static final long serialVersionUID = -6322765147037755510L; + private boolean isExpiredSession; + + public AuthenticationException() { + } + + public AuthenticationException(boolean isExpiredSession) { + this.isExpiredSession = isExpiredSession; + } + + public boolean isExpiredSession() { + return isExpiredSession; + } + } \ No newline at end of file diff --git a/symmetric-core/src/main/java/org/jumpmind/symmetric/transport/http/HttpIncomingTransport.java b/symmetric-core/src/main/java/org/jumpmind/symmetric/transport/http/HttpIncomingTransport.java index 9de521226d..df34dab5b1 100644 --- a/symmetric-core/src/main/java/org/jumpmind/symmetric/transport/http/HttpIncomingTransport.java +++ b/symmetric-core/src/main/java/org/jumpmind/symmetric/transport/http/HttpIncomingTransport.java @@ -55,14 +55,24 @@ public class HttpIncomingTransport implements IIncomingTransport { private int httpTimeout; - private String redirectionUrl; - + private String redirectionUrl; + + private String nodeId; + + private String securityToken; + public HttpIncomingTransport(HttpURLConnection connection, IParameterService parameterService) { this.connection = connection; - this.parameterService = parameterService; + this.parameterService = parameterService; this.httpTimeout = parameterService.getInt(ParameterConstants.TRANSPORT_HTTP_TIMEOUT); } - + + public HttpIncomingTransport(HttpURLConnection connection, IParameterService parameterService, String nodeId, String securityToken) { + this(connection, parameterService); + this.nodeId = nodeId; + this.securityToken = securityToken; + } + @Override public String getUrl() { return this.connection.getURL().toExternalForm(); @@ -71,17 +81,19 @@ public String getUrl() { @Override public void close() { if (reader != null) { - try { - reader.close(); - } catch(IOException e) { } - reader = null; - } - + try { + reader.close(); + } catch (IOException e) { + } + reader = null; + } + if (is != null) { - try { - is.close(); - } catch(IOException e) { } - is = null; + try { + is.close(); + } catch (IOException e) { + } + is = null; } } @@ -116,7 +128,11 @@ public InputStream openStream() throws IOException { case WebConstants.SC_SERVICE_UNAVAILABLE: throw new ServiceUnavailableException(); case WebConstants.SC_FORBIDDEN: + HttpTransportManager.clearSession(connection); throw new AuthenticationException(); + case WebConstants.SC_AUTH_EXPIRED: + HttpTransportManager.clearSession(connection); + throw new AuthenticationException(true); case WebConstants.SC_NO_CONTENT: throw new NoContentException(); case WebConstants.SC_OK: @@ -159,20 +175,16 @@ private HttpURLConnection openConnectionCheckRedirects(HttpURLConnection connect { boolean redir; int redirects = 0; - do - { + do { connection.setInstanceFollowRedirects(false); redir = false; int stat = connection.getResponseCode(); - if (stat >= 300 && stat <= 307 && stat != 306 && - stat != HttpURLConnection.HTTP_NOT_MODIFIED) - { + if (stat >= 300 && stat <= 307 && stat != 306 && stat != HttpURLConnection.HTTP_NOT_MODIFIED) { URL base = connection.getURL(); redirectionUrl = connection.getHeaderField("Location"); URL target = null; - if (redirectionUrl != null) - { + if (redirectionUrl != null) { target = new URL(base, redirectionUrl); } connection.disconnect(); @@ -180,31 +192,21 @@ private HttpURLConnection openConnectionCheckRedirects(HttpURLConnection connect // and should be limited to 5 redirections at most. if (target == null || !(target.getProtocol().equals("http") || target.getProtocol().equals("https")) - || redirects >= 5) - { + || redirects >= 5) { throw new SecurityException("illegal URL redirect"); } redir = true; - connection = HttpTransportManager.openConnection(target, getBasicAuthUsername(), getBasicAuthPassword()); + connection = HttpTransportManager.openConnection(target, nodeId, securityToken); connection.setConnectTimeout(httpTimeout); connection.setReadTimeout(httpTimeout); redirects++; } - } - while (redir); + } while (redir); return connection; } - protected String getBasicAuthUsername() { - return parameterService.getString(ParameterConstants.TRANSPORT_HTTP_BASIC_AUTH_USERNAME); - } - - protected String getBasicAuthPassword() { - return parameterService.getString(ParameterConstants.TRANSPORT_HTTP_BASIC_AUTH_PASSWORD); - } - public HttpURLConnection getConnection() { return connection; } diff --git a/symmetric-core/src/main/java/org/jumpmind/symmetric/transport/http/HttpOutgoingTransport.java b/symmetric-core/src/main/java/org/jumpmind/symmetric/transport/http/HttpOutgoingTransport.java index 7eaeef2a8f..b1b5d5d1bd 100644 --- a/symmetric-core/src/main/java/org/jumpmind/symmetric/transport/http/HttpOutgoingTransport.java +++ b/symmetric-core/src/main/java/org/jumpmind/symmetric/transport/http/HttpOutgoingTransport.java @@ -72,9 +72,9 @@ public class HttpOutgoingTransport implements IOutgoingWithResponseTransport { private int compressionLevel; - private String basicAuthUsername; + private String nodeId; - private String basicAuthPassword; + private String securityToken; private boolean streamOutputEnabled = false; @@ -85,35 +85,27 @@ public class HttpOutgoingTransport implements IOutgoingWithResponseTransport { private Map requestProperties; public HttpOutgoingTransport(URL url, int httpTimeout, boolean useCompression, - int compressionStrategy, int compressionLevel, String basicAuthUsername, - String basicAuthPassword, boolean streamOutputEnabled, int streamOutputSize, + int compressionStrategy, int compressionLevel, String nodeId, + String securityToken, boolean streamOutputEnabled, int streamOutputSize, boolean fileUpload) { this.url = url; this.httpTimeout = httpTimeout; this.useCompression = useCompression; this.compressionLevel = compressionLevel; this.compressionStrategy = compressionStrategy; - this.basicAuthUsername = basicAuthUsername; - this.basicAuthPassword = basicAuthPassword; + this.nodeId = nodeId; + this.securityToken = securityToken; this.streamOutputChunkSize = streamOutputSize; this.streamOutputEnabled = streamOutputEnabled; this.fileUpload = fileUpload; } public HttpOutgoingTransport(URL url, int httpTimeout, boolean useCompression, - int compressionStrategy, int compressionLevel, String basicAuthUsername, - String basicAuthPassword, boolean streamOutputEnabled, int streamOutputSize, + int compressionStrategy, int compressionLevel, String nodeId, + String securityToken, boolean streamOutputEnabled, int streamOutputSize, boolean fileUpload, Map requestProperties) { - this.url = url; - this.httpTimeout = httpTimeout; - this.useCompression = useCompression; - this.compressionLevel = compressionLevel; - this.compressionStrategy = compressionStrategy; - this.basicAuthUsername = basicAuthUsername; - this.basicAuthPassword = basicAuthPassword; - this.streamOutputChunkSize = streamOutputSize; - this.streamOutputEnabled = streamOutputEnabled; - this.fileUpload = fileUpload; + this(url, httpTimeout, useCompression, compressionStrategy, compressionLevel, nodeId, securityToken, + streamOutputEnabled, streamOutputSize, fileUpload); this.requestProperties = requestProperties; } @@ -129,9 +121,10 @@ public void close() { private void closeReader() { if (reader != null) { - try { - reader.close(); - } catch(IOException e) { } + try { + reader.close(); + } catch (IOException e) { + } reader = null; } } @@ -147,9 +140,10 @@ private void closeOutputStream(boolean closeQuietly) { throw new IoException(ex); } finally { if (closeQuietly) { - try { - os.close(); - } catch(IOException e) { } + try { + os.close(); + } catch (IOException e) { + } } else { try { os.close(); @@ -173,9 +167,10 @@ private void closeWriter(boolean closeQuietly) { throw new IoException(ex); } finally { if (closeQuietly) { - try { - writer.close(); - } catch(IOException e) { } + try { + writer.close(); + } catch (IOException e) { + } } else { try { writer.close(); @@ -200,8 +195,7 @@ private void closeWriter(boolean closeQuietly) { */ private HttpURLConnection requestReservation(String queue) { try { - connection = HttpTransportManager.openConnection(url, basicAuthUsername, - basicAuthPassword); + connection = HttpTransportManager.openConnection(url, nodeId, securityToken); connection.setUseCaches(false); connection.setConnectTimeout(httpTimeout); connection.setReadTimeout(httpTimeout); @@ -217,8 +211,7 @@ private HttpURLConnection requestReservation(String queue) { public OutputStream openStream() { try { - connection = HttpTransportManager.openConnection(url, basicAuthUsername, - basicAuthPassword); + connection = HttpTransportManager.openConnection(url, nodeId, securityToken); if (streamOutputEnabled) { connection.setChunkedStreamingMode(streamOutputChunkSize); } @@ -228,13 +221,12 @@ public OutputStream openStream() { connection.setConnectTimeout(httpTimeout); connection.setReadTimeout(httpTimeout); - if (this.requestProperties != null) { - for (Map.Entry requestProperty : this.requestProperties.entrySet()) { - connection.setRequestProperty(requestProperty.getKey(), requestProperty.getValue()); - } + if (requestProperties != null) { + for (Map.Entry requestProperty : requestProperties.entrySet()) { + connection.setRequestProperty(requestProperty.getKey(), requestProperty.getValue()); + } } - boundary = Long.toHexString(System.currentTimeMillis()); if (!fileUpload) { connection.setRequestMethod("PUT"); connection.setRequestProperty("Accept-Encoding", "gzip"); @@ -242,8 +234,8 @@ public OutputStream openStream() { connection.addRequestProperty("Content-Type", "gzip"); // application/x-gzip? } } else { - connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" - + boundary); + boundary = Long.toHexString(System.currentTimeMillis()); + connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary); } os = connection.getOutputStream(); @@ -301,7 +293,11 @@ private void analyzeResponseCode(int code) throws IOException { } else if (WebConstants.SC_NO_RESERVATION == code) { throw new NoReservationException(); } else if (WebConstants.SC_FORBIDDEN == code) { + HttpTransportManager.clearSession(connection); throw new AuthenticationException(); + } else if (WebConstants.SC_AUTH_EXPIRED == code) { + HttpTransportManager.clearSession(connection); + throw new AuthenticationException(true); } else if (WebConstants.SYNC_DISABLED == code) { throw new SyncDisabledException(); } else if (WebConstants.REGISTRATION_REQUIRED == code) { @@ -338,12 +334,9 @@ public ChannelMap getSuspendIgnoreChannelLists(IConfigurationService configurati suspendIgnoreChannelsList.addSuspendChannels(suspends); suspendIgnoreChannelsList.addIgnoreChannels(ignores); - ChannelMap localSuspendIgnoreChannelsList = configurationService - .getSuspendIgnoreChannelLists(targetNode.getNodeId()); - suspendIgnoreChannelsList.addSuspendChannels( - localSuspendIgnoreChannelsList.getSuspendChannels()); - suspendIgnoreChannelsList.addIgnoreChannels( - localSuspendIgnoreChannelsList.getIgnoreChannels()); + ChannelMap localSuspendIgnoreChannelsList = configurationService.getSuspendIgnoreChannelLists(targetNode.getNodeId()); + suspendIgnoreChannelsList.addSuspendChannels(localSuspendIgnoreChannelsList.getSuspendChannels()); + suspendIgnoreChannelsList.addIgnoreChannels(localSuspendIgnoreChannelsList.getIgnoreChannels()); return suspendIgnoreChannelsList; } diff --git a/symmetric-core/src/main/java/org/jumpmind/symmetric/transport/http/HttpTransportManager.java b/symmetric-core/src/main/java/org/jumpmind/symmetric/transport/http/HttpTransportManager.java index 83a6b1f862..6f2945cb05 100644 --- a/symmetric-core/src/main/java/org/jumpmind/symmetric/transport/http/HttpTransportManager.java +++ b/symmetric-core/src/main/java/org/jumpmind/symmetric/transport/http/HttpTransportManager.java @@ -27,20 +27,22 @@ import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; +import java.net.CookieHandler; +import java.net.CookieManager; +import java.net.HttpCookie; import java.net.HttpURLConnection; +import java.net.URISyntaxException; import java.net.URL; import java.net.URLEncoder; import java.util.List; import java.util.Map; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; import java.util.zip.GZIPInputStream; -import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang.StringUtils; import org.jumpmind.exception.IoException; import org.jumpmind.symmetric.AbstractSymmetricEngine; import org.jumpmind.symmetric.ISymmetricEngine; +import org.jumpmind.symmetric.Version; import org.jumpmind.symmetric.common.Constants; import org.jumpmind.symmetric.common.ParameterConstants; import org.jumpmind.symmetric.io.IoConstants; @@ -54,6 +56,8 @@ import org.jumpmind.symmetric.transport.TransportUtils; import org.jumpmind.symmetric.web.WebConstants; import org.jumpmind.util.AppUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Allow remote communication to nodes, in order to push data, pull data, and @@ -61,11 +65,13 @@ */ public class HttpTransportManager extends AbstractTransportManager implements ITransportManager { + protected static final Logger log = LoggerFactory.getLogger(HttpTransportManager.class); + protected ISymmetricEngine engine; - private AtomicReference cachedHostName = new AtomicReference(); - private AtomicReference cachedIpAddress = new AtomicReference(); - private AtomicLong cacheTime = new AtomicLong(-1); - private long hostCacheTtl = 0; + + protected static boolean useHeaderSecurityToken; + + protected static boolean useSessionAuth; public HttpTransportManager() { } @@ -73,7 +79,8 @@ public HttpTransportManager() { public HttpTransportManager(ISymmetricEngine engine) { super(engine.getExtensionService()); this.engine = engine; - hostCacheTtl = engine.getParameterService().getLong("cache.security.token.host.time.ms", 5*60*1000); + useHeaderSecurityToken = engine.getParameterService().is(ParameterConstants.TRANSPORT_HTTP_USE_HEADER_SECURITY_TOKEN); + useSessionAuth = engine.getParameterService().is(ParameterConstants.TRANSPORT_HTTP_USE_SESSION_AUTH); } public int sendCopyRequest(Node local) throws IOException { @@ -87,21 +94,18 @@ public int sendCopyRequest(Node local) throws IOException { } String securityToken = engine.getNodeService().findNodeSecurity(local.getNodeId()) .getNodePassword(); - String url = addSecurityToken(engine.getParameterService().getRegistrationUrl() + "/copy", - "&", local.getNodeId(), securityToken); + String url = addNodeInfo(engine.getParameterService().getRegistrationUrl() + "/copy", local.getNodeId(), securityToken, false); url = add(url, WebConstants.EXTERNAL_ID, engine.getParameterService().getExternalId(), "&"); url = add(url, WebConstants.NODE_GROUP_ID, engine.getParameterService().getNodeGroupId(), "&"); log.info("Contact server to do node copy using a url of: " + url); - return sendMessage(new URL(url), data.toString()); + return sendMessage(new URL(url), local.getNodeId(), securityToken, data.toString()); } @Override public int sendStatusRequest(Node local, Map statuses) throws IOException { - String securityToken = engine.getNodeService().findNodeSecurity(local.getNodeId()) - .getNodePassword(); - String url = addSecurityToken(engine.getParameterService().getRegistrationUrl() + "/pushstatus/", - "&", local.getNodeId(), securityToken); + String securityToken = engine.getNodeService().findNodeSecurity(local.getNodeId()).getNodePassword(); + String url = addNodeInfo(engine.getParameterService().getRegistrationUrl() + "/pushstatus/", local.getNodeId(), securityToken, false); url = add(url, WebConstants.EXTERNAL_ID, engine.getParameterService().getExternalId(), "&"); url = add(url, WebConstants.NODE_GROUP_ID, engine.getParameterService().getNodeGroupId(), "&"); @@ -110,7 +114,7 @@ public int sendStatusRequest(Node local, Map statuses) throws IO } log.debug("Sending status with URL: " + url); - return sendMessage(new URL(url), ""); + return sendMessage(new URL(url), local.getNodeId(), securityToken, ""); } public int sendAcknowledgement(Node remote, List list, Node local, @@ -132,12 +136,11 @@ public void writeAcknowledgement(OutputStream out, Node remote, List cookies = null; + try { + cookies = getCookieManager().getCookieStore().get(connection.getURL().toURI()); + } catch (URISyntaxException e) { + log.error("Bad URL", e); + } + if (cookies != null) { + for (HttpCookie cookie : cookies) { + if (cookie.getName().startsWith(WebConstants.SESSION_PREFIX)) { + hasSession = true; + break; + } + } + } + } + return hasSession; } public int getOutputStreamSize() { @@ -202,14 +240,6 @@ public int getCompressionStrategy() { return engine.getParameterService().getInt(ParameterConstants.TRANSPORT_HTTP_COMPRESSION_STRATEGY); } - public String getBasicAuthUsername() { - return engine.getParameterService().getString(ParameterConstants.TRANSPORT_HTTP_BASIC_AUTH_USERNAME); - } - - public String getBasicAuthPassword() { - return engine.getParameterService().getString(ParameterConstants.TRANSPORT_HTTP_BASIC_AUTH_PASSWORD); - } - public void writeMessage(OutputStream out, String data) throws IOException { PrintWriter pw = new PrintWriter(new OutputStreamWriter(out, IoConstants.ENCODING), true); pw.println(data); @@ -218,26 +248,26 @@ public void writeMessage(OutputStream out, String data) throws IOException { public IIncomingTransport getFilePullTransport(Node remote, Node local, String securityToken, Map requestProperties, String registrationUrl) throws IOException { - HttpURLConnection conn = createGetConnectionFor(new URL(buildURL("filesync/pull", remote, local, - securityToken, registrationUrl))); + HttpURLConnection conn = createGetConnectionFor(new URL(buildURL("filesync/pull", remote, local, securityToken, registrationUrl)), + local.getNodeId(), securityToken); if (requestProperties != null) { for (String key : requestProperties.keySet()) { conn.addRequestProperty(key, requestProperties.get(key)); } } - return new HttpIncomingTransport(conn, engine.getParameterService()); + return new HttpIncomingTransport(conn, engine.getParameterService(), local.getNodeId(), securityToken); } public IIncomingTransport getPullTransport(Node remote, Node local, String securityToken, Map requestProperties, String registrationUrl) throws IOException { - HttpURLConnection conn = createGetConnectionFor(new URL(buildURL("pull", remote, local, - securityToken, registrationUrl))); + HttpURLConnection conn = createGetConnectionFor(new URL(buildURL("pull", remote, local, securityToken, registrationUrl)), + local.getNodeId(), securityToken); if (requestProperties != null) { for (String key : requestProperties.keySet()) { conn.addRequestProperty(key, requestProperties.get(key)); } } - return new HttpIncomingTransport(conn, engine.getParameterService()); + return new HttpIncomingTransport(conn, engine.getParameterService(), local.getNodeId(), securityToken); } public IIncomingTransport getPingTransport(Node remote, Node local, String registrationUrl) throws IOException { @@ -250,38 +280,36 @@ public IOutgoingWithResponseTransport getPushTransport(Node remote, Node local, String registrationUrl) throws IOException { URL url = new URL(buildURL("push", remote, local, securityToken, registrationUrl)); return new HttpOutgoingTransport(url, getHttpTimeOutInMs(), isUseCompression(remote), - getCompressionStrategy(), getCompressionLevel(), getBasicAuthUsername(), - getBasicAuthPassword(), isOutputStreamEnabled(), getOutputStreamSize(), false, requestProperties); + getCompressionStrategy(), getCompressionLevel(), local.getNodeId(), + securityToken, isOutputStreamEnabled(), getOutputStreamSize(), false, requestProperties); } public IOutgoingWithResponseTransport getPushTransport(Node remote, Node local, String securityToken, String registrationUrl) throws IOException { URL url = new URL(buildURL("push", remote, local, securityToken, registrationUrl)); return new HttpOutgoingTransport(url, getHttpTimeOutInMs(), isUseCompression(remote), - getCompressionStrategy(), getCompressionLevel(), getBasicAuthUsername(), - getBasicAuthPassword(), isOutputStreamEnabled(), getOutputStreamSize(), false); + getCompressionStrategy(), getCompressionLevel(), local.getNodeId(), + securityToken, isOutputStreamEnabled(), getOutputStreamSize(), false); } public IOutgoingWithResponseTransport getFilePushTransport(Node remote, Node local, String securityToken, String registrationUrl) throws IOException { URL url = new URL(buildURL("filesync/push", remote, local, securityToken, registrationUrl)); return new HttpOutgoingTransport(url, getHttpTimeOutInMs(), isUseCompression(remote), - getCompressionStrategy(), getCompressionLevel(), getBasicAuthUsername(), - getBasicAuthPassword(), isOutputStreamEnabled(), getOutputStreamSize(), true); + getCompressionStrategy(), getCompressionLevel(), local.getNodeId(), + securityToken, isOutputStreamEnabled(), getOutputStreamSize(), true); } public IIncomingTransport getConfigTransport(Node remote, Node local, String securityToken, String symmetricVersion, String configVersion, String registrationUrl) throws IOException { - StringBuilder builder = new StringBuilder(buildURL("config", remote, local, - securityToken, registrationUrl)); + StringBuilder builder = new StringBuilder(buildURL("config", remote, local, securityToken, registrationUrl)); append(builder, WebConstants.SYMMETRIC_VERSION, symmetricVersion); append(builder, WebConstants.CONFIG_VERSION, configVersion); - HttpURLConnection conn = createGetConnectionFor(new URL(builder.toString())); + HttpURLConnection conn = createGetConnectionFor(new URL(builder.toString()), local.getNodeId(), securityToken); return new HttpIncomingTransport(conn, engine.getParameterService()); } - public IIncomingTransport getRegisterTransport(Node node, String registrationUrl) - throws IOException { + public IIncomingTransport getRegisterTransport(Node node, String registrationUrl) throws IOException { return new HttpIncomingTransport(createGetConnectionFor(new URL(buildRegistrationUrl( registrationUrl, node))), engine.getParameterService()); } @@ -305,16 +333,19 @@ public static String buildRegistrationUrl(String baseUrl, Node node) throws IOEx return builder.toString(); } - protected HttpURLConnection createGetConnectionFor(URL url) throws IOException { - HttpURLConnection conn = HttpTransportManager.openConnection(url, getBasicAuthUsername(), - getBasicAuthPassword()); + protected HttpURLConnection createGetConnectionFor(URL url, String nodeId, String securityToken) throws IOException { + HttpURLConnection conn = HttpTransportManager.openConnection(url, nodeId, securityToken); conn.setRequestProperty("accept-encoding", "gzip"); conn.setConnectTimeout(getHttpTimeOutInMs()); conn.setReadTimeout(getHttpTimeOutInMs()); conn.setRequestMethod("GET"); return conn; } - + + protected HttpURLConnection createGetConnectionFor(URL url) throws IOException { + return createGetConnectionFor(url, null, null); + } + protected static InputStream getInputStreamFrom(HttpURLConnection connection) throws IOException { String type = connection.getContentEncoding(); InputStream in = connection.getInputStream(); @@ -337,58 +368,22 @@ protected static BufferedReader getReaderFrom(HttpURLConnection connection) thro } /** - * Build a url for an action. Include the nodeid and the security token. + * Build a url for an action. */ - protected String buildURL(String action, Node remote, Node local, String securityToken, - String registrationUrl) throws IOException { - String url = addSecurityToken((resolveURL(remote.getSyncUrl(), registrationUrl) + "/" + action), - "&", local.getNodeId(), securityToken); + protected String buildURL(String action, Node remote, Node local, String securityToken, String registrationUrl) throws IOException { + boolean forceParamSecurityToken = Version.isOlderMinorVersion(remote.getSymmetricVersion(), "3.11"); + String url = addNodeInfo((resolveURL(remote.getSyncUrl(), registrationUrl) + "/" + action), local.getNodeId(), securityToken, + forceParamSecurityToken); log.debug("Building transport url: {}", url); return url; } - protected String addSecurityToken(String base, String connector, String nodeId, - String securityToken) { + protected String addNodeInfo(String base, String nodeId, String securityToken, boolean forceParamSecurityToken) { StringBuilder sb = new StringBuilder(addNodeId(base, nodeId, "?")); - sb.append(connector); - sb.append(WebConstants.SECURITY_TOKEN); - sb.append("="); - sb.append(securityToken); - String[] hostAndIpAddress = getHostAndIpAddress(); - append(sb, WebConstants.HOST_NAME, hostAndIpAddress[0]); - append(sb, WebConstants.IP_ADDRESS, hostAndIpAddress[1]); - return sb.toString(); - } - - protected String[] getHostAndIpAddress() { - String hostName, ipAddress; - if (cachedHostName.get() == null || cachedIpAddress.get() == null || cacheTimeExpired(cacheTime)) { - cachedHostName.set(null); - cachedIpAddress.set(null); - hostName = AppUtils.getHostName(); - ipAddress = AppUtils.getIpAddress(); - if (!StringUtils.isEmpty(hostName) && !"unknown".equals(hostName)) { - cachedHostName.set(hostName); - } - if (!StringUtils.isEmpty(ipAddress) && !"unknown".equals(ipAddress)) { - cachedIpAddress.set(ipAddress); - } - cacheTime.set(System.currentTimeMillis()); - } else { - hostName = cachedHostName.get(); - ipAddress = cachedIpAddress.get(); - } - - return new String[] {hostName, ipAddress}; - } - - protected boolean cacheTimeExpired(AtomicLong argCacheTime) { - long cacheTimeLong = argCacheTime.get(); - if (cacheTimeLong == -1 || (System.currentTimeMillis()-cacheTimeLong) > hostCacheTtl) { - return true; - } else { - return false; + if (!useHeaderSecurityToken || forceParamSecurityToken) { + sb.append("&").append(WebConstants.SECURITY_TOKEN).append("=").append(securityToken); } + return sb.toString(); } protected String addNodeId(String base, String nodeId, String connector) { diff --git a/symmetric-core/src/main/java/org/jumpmind/symmetric/web/WebConstants.java b/symmetric-core/src/main/java/org/jumpmind/symmetric/web/WebConstants.java index 97a7e44ed1..9e445cd13b 100644 --- a/symmetric-core/src/main/java/org/jumpmind/symmetric/web/WebConstants.java +++ b/symmetric-core/src/main/java/org/jumpmind/symmetric/web/WebConstants.java @@ -55,6 +55,8 @@ public class WebConstants { public static final int SC_FORBIDDEN = 659; + public static final int SC_AUTH_EXPIRED = 669; + public static final int SC_SERVICE_UNAVAILABLE = 660; public static final int SC_SERVICE_BUSY = 670; @@ -136,7 +138,7 @@ public class WebConstants { public static final String DEPLOYMENT_TYPE = "deploymentType"; public static final String SECURITY_TOKEN = "securityToken"; - + public static final String SUSPENDED_CHANNELS = "Suspended-Channels"; public static final String IGNORED_CHANNELS = "Ignored-Channels"; @@ -146,5 +148,7 @@ public class WebConstants { public static final String CHANNEL_QUEUE = "threadChannel"; public static final String CONFIG_VERSION = "configVersion"; + + public static final String SESSION_PREFIX = "JSESSIONID_"; } \ No newline at end of file diff --git a/symmetric-core/src/main/resources/symmetric-default.properties b/symmetric-core/src/main/resources/symmetric-default.properties index 105ad7c5d9..5f40c50508 100644 --- a/symmetric-core/src/main/resources/symmetric-default.properties +++ b/symmetric-core/src/main/resources/symmetric-default.properties @@ -418,6 +418,33 @@ transport.max.bytes.to.sync=104857600 # Type: integer transport.max.error.millis=300000 +# When authenticating a client node, create a session on the server and give a cookie to the client +# that can be sent with subsequent requests. This avoids authenticating every request and limits +# how often the security token is sent. +# +# DatabaseOverridable: false +# Tags: transport +# Type: boolean +http.use.session.auth=true + +# How long in seconds an authenticated node can keep a session before it expires and the node must be +# authenticated again. A value of zero or less means never expire. +# +# DatabaseOverridable: false +# Tags: transport +# Type: integer +http.session.expire.seconds=14400 + +# When authenticating to a server node, send the security token in the request header instead +# of using a URL parameter. Using the request header avoids accidentally logging the +# security token. The transport uses the remote node's version to determine if it should use header or parameter. +# Set this to false to force usage of the older parameter style of authentication. +# +# DatabaseOverridable: false +# Tags: transport +# Type: boolean +http.use.header.security.token=true + # This indicates whether this node engine should be started when the instance is restarted # # DatabaseOverridable: true diff --git a/symmetric-server/src/main/java/org/jumpmind/symmetric/SymmetricLauncher.java b/symmetric-server/src/main/java/org/jumpmind/symmetric/SymmetricLauncher.java index f415d5d902..efb07c03f3 100644 --- a/symmetric-server/src/main/java/org/jumpmind/symmetric/SymmetricLauncher.java +++ b/symmetric-server/src/main/java/org/jumpmind/symmetric/SymmetricLauncher.java @@ -47,10 +47,6 @@ public class SymmetricLauncher extends AbstractCommandLauncher { private static final String OPTION_PORT_SERVER = "port"; private static final String OPTION_HOST_SERVER = "host"; - - private static final String OPTION_HTTP_BASIC_AUTH_USER = "http-basic-auth-user"; - - private static final String OPTION_HTTP_BASIC_AUTH_PASSWORD = "http-basic-auth-password"; private static final String OPTION_SECURE_PORT_SERVER = "secure-port"; @@ -114,8 +110,6 @@ protected void buildOptions(Options options) { addOption(options, "I", OPTION_MAX_IDLE_TIME, true); addOption(options, "nnio", OPTION_NO_NIO, false); addOption(options, "ndb", OPTION_NO_DIRECT_BUFFER, false); - addOption(options, "hbau", OPTION_HTTP_BASIC_AUTH_USER, true); - addOption(options, "hbap", OPTION_HTTP_BASIC_AUTH_PASSWORD, true); addOption(options, "JD", OPTION_JMX_DISABLE, false); addOption(options, "J", OPTION_JMX_PORT, true); addOption(options, OPTION_WINXP, OPTION_WINXP, false); @@ -133,18 +127,10 @@ protected boolean executeWithOptions(CommandLine line) throws Exception { int maxIdleTime = SymmetricWebServer.DEFAULT_MAX_IDLE_TIME; boolean noNio = false; boolean noDirectBuffer = false; - String httpBasicAuthUser = null; - String httpBasicAuthPassword = null; configureCrypto(line); removeOldHeapDumps(); - if (line.hasOption(OPTION_HTTP_BASIC_AUTH_USER) && - line.hasOption(OPTION_HTTP_BASIC_AUTH_PASSWORD)) { - httpBasicAuthUser = line.getOptionValue(OPTION_HTTP_BASIC_AUTH_USER); - httpBasicAuthPassword = line.getOptionValue(OPTION_HTTP_BASIC_AUTH_PASSWORD); - } - if (line.hasOption(OPTION_HOST_SERVER)) { host = line.getOptionValue(OPTION_HOST_SERVER); } @@ -217,8 +203,6 @@ public void run() { if (isNotBlank(host)) { webServer.setHost(host); } - webServer.setBasicAuthUsername(httpBasicAuthUser); - webServer.setBasicAuthPassword(httpBasicAuthPassword); if (jmxDisabledFlag) { webServer.setJmxEnabled(false); diff --git a/symmetric-server/src/main/java/org/jumpmind/symmetric/SymmetricWebServer.java b/symmetric-server/src/main/java/org/jumpmind/symmetric/SymmetricWebServer.java index c6612b954d..7c5edbf41b 100644 --- a/symmetric-server/src/main/java/org/jumpmind/symmetric/SymmetricWebServer.java +++ b/symmetric-server/src/main/java/org/jumpmind/symmetric/SymmetricWebServer.java @@ -38,11 +38,6 @@ import org.apache.commons.lang.ClassUtils; import org.apache.commons.lang.StringUtils; import org.eclipse.jetty.http.HttpVersion; -import org.eclipse.jetty.security.ConstraintMapping; -import org.eclipse.jetty.security.ConstraintSecurityHandler; -import org.eclipse.jetty.security.HashLoginService; -import org.eclipse.jetty.security.UserStore; -import org.eclipse.jetty.security.authentication.BasicAuthenticator; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; @@ -51,8 +46,6 @@ import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.servlet.FilterHolder; -import org.eclipse.jetty.util.security.Constraint; -import org.eclipse.jetty.util.security.Password; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.webapp.WebAppContext; import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer; @@ -262,8 +255,6 @@ public SymmetricWebServer start(int httpPort, int securePort, int httpJmxPort, M server.setConnectors(getConnectors(server, httpPort, securePort, mode)); - setupBasicAuthIfNeeded(server); - webapp = new WebAppContext(); webapp.setParentLoaderPriority(true); webapp.setConfigurationDiscovered(true); @@ -296,7 +287,7 @@ public SymmetricWebServer start(int httpPort, int securePort, int httpJmxPort, M .setInitParameter(WebConstants.INIT_PARAM_MULTI_SERVER_MODE, Boolean.toString(true)); } - webapp.getSessionHandler().getSessionCookieConfig().setName("JSESSIONID_" + (httpsPort > 0 && httpsEnabled ? httpsPort : "") + (httpEnabled && httpPort > 0 ? "_" + httpPort : "")); + webapp.getSessionHandler().getSessionCookieConfig().setName(WebConstants.SESSION_PREFIX + (httpsPort > 0 && httpsEnabled ? httpsPort : "") + (httpEnabled && httpPort > 0 ? "_" + httpPort : "")); server.setHandler(webapp); @@ -380,35 +371,6 @@ public void waitForEnginesToComeOnline(long maxWaitTimeInMs) throws InterruptedE } } - protected void setupBasicAuthIfNeeded(Server server) { - if (StringUtils.isNotBlank(basicAuthUsername)) { - ConstraintSecurityHandler sh = new ConstraintSecurityHandler(); - - Constraint constraint = new Constraint(); - constraint.setName(Constraint.__BASIC_AUTH); - - constraint.setRoles(new String[] { SecurityConstants.EMBEDDED_WEBSERVER_DEFAULT_ROLE }); - constraint.setAuthenticate(true); - - ConstraintMapping cm = new ConstraintMapping(); - cm.setConstraint(constraint); - cm.setPathSpec("/*"); - // sh.setConstraintMappings(new ConstraintMapping[] {cm}); - sh.addConstraintMapping(cm); - - sh.setAuthenticator(new BasicAuthenticator()); - - HashLoginService loginService = new HashLoginService(); - UserStore userStore = new UserStore(); - userStore.addUser(basicAuthUsername, new Password(basicAuthPassword), null); - loginService.setUserStore(userStore); - sh.setLoginService(loginService); - - server.setHandler(sh); - - } - } - protected Connector[] getConnectors(Server server, int port, int securePort, Mode mode) { ArrayList connectors = new ArrayList(); @@ -569,14 +531,6 @@ public void setHost(String host) { this.host = host; } - public void setBasicAuthPassword(String basicAuthPassword) { - this.basicAuthPassword = basicAuthPassword; - } - - public void setBasicAuthUsername(String basicAuthUsername) { - this.basicAuthUsername = basicAuthUsername; - } - public void setWebAppDir(String webAppDir) { this.webAppDir = webAppDir; } diff --git a/symmetric-server/src/main/java/org/jumpmind/symmetric/web/AuthenticationInterceptor.java b/symmetric-server/src/main/java/org/jumpmind/symmetric/web/AuthenticationInterceptor.java index e6021beb64..17a301e041 100644 --- a/symmetric-server/src/main/java/org/jumpmind/symmetric/web/AuthenticationInterceptor.java +++ b/symmetric-server/src/main/java/org/jumpmind/symmetric/web/AuthenticationInterceptor.java @@ -25,6 +25,7 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; import org.apache.commons.lang.StringUtils; import org.jumpmind.symmetric.service.INodeService; @@ -41,22 +42,58 @@ public class AuthenticationInterceptor implements IInterceptor { private INodeService nodeService; - public AuthenticationInterceptor(INodeService nodeService) { + private boolean useSessionAuth; + + private int sessionExpireMillis; + + public AuthenticationInterceptor(INodeService nodeService, boolean useSessionAuth, int sessionExpireSeconds) { this.nodeService = nodeService; + this.useSessionAuth = useSessionAuth; + this.sessionExpireMillis = sessionExpireSeconds * 1000; } - public boolean before(HttpServletRequest req, HttpServletResponse resp) throws IOException, - ServletException { - String securityToken = req.getParameter(WebConstants.SECURITY_TOKEN); - String nodeId = req.getParameter(WebConstants.NODE_ID); + public boolean before(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { - if (StringUtils.isEmpty(securityToken) || StringUtils.isEmpty(nodeId)) { - ServletUtils.sendError(resp, HttpServletResponse.SC_FORBIDDEN); - return false; - } + HttpSession session = null; + String nodeId = null; + AuthenticationStatus status = AuthenticationStatus.FORBIDDEN; - AuthenticationStatus status = nodeService.getAuthenticationStatus(nodeId, securityToken); + if (useSessionAuth) { + session = req.getSession(false); + if (session != null) { + nodeId = (String) session.getAttribute(WebConstants.NODE_ID); + if ((sessionExpireMillis > 0 && System.currentTimeMillis() - session.getCreationTime() > sessionExpireMillis) + || StringUtils.isEmpty(nodeId)) { + log.debug("Node '{}' needs to renew authentication", nodeId); + session.invalidate(); + ServletUtils.sendError(resp, WebConstants.SC_AUTH_EXPIRED); + return false; + } + } + } + if (nodeId != null) { + status = AuthenticationStatus.ACCEPTED; + } else { + String securityToken = req.getHeader(WebConstants.SECURITY_TOKEN); + if (securityToken == null) { + securityToken = req.getParameter(WebConstants.SECURITY_TOKEN); + } + nodeId = req.getParameter(WebConstants.NODE_ID); + + if (StringUtils.isEmpty(securityToken) || StringUtils.isEmpty(nodeId)) { + ServletUtils.sendError(resp, WebConstants.SC_FORBIDDEN); + return false; + } + + status = nodeService.getAuthenticationStatus(nodeId, securityToken); + + if (useSessionAuth && AuthenticationStatus.ACCEPTED.equals(status)) { + session = req.getSession(); + session.setAttribute(WebConstants.NODE_ID, nodeId); + } + } + if (AuthenticationStatus.ACCEPTED.equals(status)) { log.debug("Node '{}' successfully authenticated", nodeId); return true; diff --git a/symmetric-server/src/main/java/org/jumpmind/symmetric/web/ServerSymmetricEngine.java b/symmetric-server/src/main/java/org/jumpmind/symmetric/web/ServerSymmetricEngine.java index 08edac276f..0694cffce6 100644 --- a/symmetric-server/src/main/java/org/jumpmind/symmetric/web/ServerSymmetricEngine.java +++ b/symmetric-server/src/main/java/org/jumpmind/symmetric/web/ServerSymmetricEngine.java @@ -79,7 +79,9 @@ protected SecurityServiceType getSecurityServiceType() { protected void init() { super.init(); - AuthenticationInterceptor authInterceptor = new AuthenticationInterceptor(nodeService); + boolean useSessionAuth = parameterService.is(ParameterConstants.TRANSPORT_HTTP_USE_SESSION_AUTH); + int sessionExpireSeconds = parameterService.getInt(ParameterConstants.TRANSPORT_HTTP_SESSION_EXPIRE_SECONDS); + AuthenticationInterceptor authInterceptor = new AuthenticationInterceptor(nodeService, useSessionAuth, sessionExpireSeconds); NodeConcurrencyInterceptor concurrencyInterceptor = new NodeConcurrencyInterceptor( concurrentConnectionManager, configurationService, statisticManager); IInterceptor[] customInterceptors = buildCustomInterceptors();