diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastConfiguration.java b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastConfiguration.java index 674d897a9..5f11e7069 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastConfiguration.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastConfiguration.java @@ -69,6 +69,7 @@ public abstract class BroadcastConfiguration protected LongProperty mMaximumRecordingAge = new SimpleLongProperty(10 * 60 * 1000); //10 minutes default protected BooleanProperty mEnabled = new SimpleBooleanProperty(false); protected BooleanProperty mValid = new SimpleBooleanProperty(); + protected BooleanProperty mTlsEnabled = new SimpleBooleanProperty(); private int mId = ++UNIQUE_ID; public BroadcastConfiguration() @@ -162,6 +163,14 @@ public BooleanProperty validProperty() return mValid; } + /** + * Configuration TLS property + */ + public BooleanProperty tlsProperty() + { + return mTlsEnabled; + } + /** * Broadcast server type */ @@ -354,6 +363,20 @@ public void setEnabled(boolean enabled) mEnabled.set(enabled); } + /** + * Indicates if this broadcaster is using TLS in its protocol. + */ + @JacksonXmlProperty(isAttribute = true, localName = "tlsEnabled") + public boolean isTlsEnabled() + { + return mTlsEnabled.get(); + } + + public void setTLSEnabled(boolean tlsEnabled) + { + mTlsEnabled.set(tlsEnabled); + } + @Override public boolean equals(Object o) { diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/ConfiguredBroadcast.java b/src/main/java/io/github/dsheirer/audio/broadcast/ConfiguredBroadcast.java index c1f4869b4..784de82d3 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/ConfiguredBroadcast.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/ConfiguredBroadcast.java @@ -68,6 +68,15 @@ public BooleanProperty enabledProperty() return mBroadcastConfiguration.enabledProperty(); } + /** + * Enabled state of the tls configuration + */ + public BooleanProperty tlsProperty() + { + return mBroadcastConfiguration.tlsProperty(); + } + + /** * Name of the broadcast configuration */ @@ -147,7 +156,7 @@ public boolean hasAudioBroadcaster() */ public static Callback extractor() { - return (ConfiguredBroadcast b) -> new Observable[] {b.nameProperty(), b.enabledProperty(), + return (ConfiguredBroadcast b) -> new Observable[] {b.nameProperty(), b.enabledProperty(), b.tlsProperty(), b.broadcastStateProperty(), b.getBroadcastConfiguration().validProperty()}; } } diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastBroadcastMetadataUpdater.java b/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastBroadcastMetadataUpdater.java index ab97502e4..0e32d24fd 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastBroadcastMetadataUpdater.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastBroadcastMetadataUpdater.java @@ -21,6 +21,7 @@ */ package io.github.dsheirer.audio.broadcast.icecast; +import com.google.common.base.Charsets; import com.google.common.base.Joiner; import io.github.dsheirer.alias.Alias; import io.github.dsheirer.alias.AliasList; @@ -37,7 +38,6 @@ import org.slf4j.LoggerFactory; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; @@ -48,7 +48,6 @@ public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdater { private final static Logger mLog = LoggerFactory.getLogger(IcecastBroadcastMetadataUpdater.class); - private final static String UTF8 = "UTF-8"; private HttpClient mHttpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); private IcecastConfiguration mIcecastConfiguration; private AliasModel mAliasModel; @@ -77,87 +76,69 @@ public IcecastBroadcastMetadataUpdater(IcecastConfiguration icecastConfiguration public void update(IdentifierCollection identifierCollection) { StringBuilder sb = new StringBuilder(); + sb.append("http://"); + sb.append(mIcecastConfiguration.getHost()); + sb.append(":"); + sb.append(mIcecastConfiguration.getPort()); + sb.append("/admin/metadata?mode=updinfo&mount="); + sb.append(URLEncoder.encode(mIcecastConfiguration.getMountPoint(), Charsets.UTF_8)); + sb.append("&charset=UTF%2d8"); + sb.append("&song=").append(URLEncoder.encode(getSong(identifierCollection), Charsets.UTF_8)); + + final String metadataUpdateURL = sb.toString(); + URI uri = URI.create(metadataUpdateURL); + + ThreadPool.SCHEDULED.submit(() -> { + try + { + HttpRequest request = HttpRequest.newBuilder() + .uri(uri) + .header(IcecastHeader.AUTHORIZATION.getValue(), mIcecastConfiguration.getBase64EncodedCredentials()) + .header(IcecastHeader.USER_AGENT.getValue(), SystemProperties.getInstance().getApplicationName()) + .GET() + .build(); - try - { - sb.append("http://"); - sb.append(mIcecastConfiguration.getHost()); - sb.append(":"); - sb.append(mIcecastConfiguration.getPort()); - sb.append("/admin/metadata?mode=updinfo&mount="); - sb.append(URLEncoder.encode(mIcecastConfiguration.getMountPoint(), UTF8)); - sb.append("&charset=UTF%2d8"); - sb.append("&song=").append(URLEncoder.encode(getSong(identifierCollection), UTF8)); - } - catch(UnsupportedEncodingException uee) - { - mLog.error("Error encoding metadata information to UTF-8", uee); - sb = null; - } - - if(sb != null) - { - final String metadataUpdateURL = sb.toString(); - URI uri = URI.create(metadataUpdateURL); + HttpResponse response = null; - ThreadPool.SCHEDULED.submit(new Runnable() - { - @Override - public void run() + try { - try + response = mHttpClient.send(request, HttpResponse.BodyHandlers.ofString()); + } + catch(IOException ioe) + { + if(!mConnectionLoggingSuppressed) { - HttpRequest request = HttpRequest.newBuilder() - .uri(uri) - .header(IcecastHeader.AUTHORIZATION.getValue(), mIcecastConfiguration.getBase64EncodedCredentials()) - .header(IcecastHeader.USER_AGENT.getValue(), SystemProperties.getInstance().getApplicationName()) - .GET() - .build(); - - HttpResponse response = null; - - try - { - response = mHttpClient.send(request, HttpResponse.BodyHandlers.ofString()); - } - catch(IOException ioe) - { - if(!mConnectionLoggingSuppressed) - { - mLog.error("IO Error submitting Icecast metadata update [" + - (metadataUpdateURL != null ? metadataUpdateURL : "no url"), ioe); - mConnectionLoggingSuppressed = true; - } - } - catch(InterruptedException ie) - { - mLog.error("Interrupted Exception Error", ie); - } + mLog.error("IO Error submitting Icecast metadata update [{}] ", metadataUpdateURL, ioe); + mConnectionLoggingSuppressed = true; + } + } + catch(InterruptedException ie) + { + mLog.error("Interrupted Exception Error", ie); + } - if(response != null) - { - if(response.statusCode() == 200) - { - mConnectionLoggingSuppressed = false; - } - else - { - if(!mConnectionLoggingSuppressed) - { - mLog.info("Error submitting Icecast 2 Metadata update to URL [" + metadataUpdateURL + - "] HTTP Response Code [" + response.statusCode() + "] Body [" + response.body() + "]"); - mConnectionLoggingSuppressed = true; - } - } - } + if(response != null) + { + if(response.statusCode() == 200) + { + mConnectionLoggingSuppressed = false; } - catch(Throwable t) + else { - mLog.error("There was an error submitting an Icecast metadata update", t); + if(!mConnectionLoggingSuppressed) + { + mLog.info("Error submitting Icecast 2 Metadata update to URL [" + metadataUpdateURL + + "] HTTP Response Code [" + response.statusCode() + "] Body [" + response.body() + "]"); + mConnectionLoggingSuppressed = true; + } } } - }); - } + } + catch(Throwable t) + { + mLog.error("There was an error submitting an Icecast metadata update", t); + } + }); } /** diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastConfiguration.java b/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastConfiguration.java index c7a915810..754e4de7b 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastConfiguration.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastConfiguration.java @@ -45,6 +45,7 @@ public abstract class IcecastConfiguration extends BroadcastConfiguration private int mChannels = 1; private int mSampleRate = 8000; private String mURL; + private boolean mIsTlsStatus; public IcecastConfiguration(BroadcastFormat format) { @@ -260,6 +261,16 @@ public String getURL() return mURL; } + @JacksonXmlProperty(isAttribute = true, localName = "tls_enabled") + public boolean isTlsEnabled() + { + return mIsTlsStatus; + } + + public void setTlsStatus(boolean tlsStatus){ + this.mIsTlsStatus = tlsStatus; + } + /** * URL associated with the broadcastAudio where users can find additional details. * diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastHTTPAudioBroadcaster.java b/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastHTTPAudioBroadcaster.java index 4ef5f396d..f4d61adbd 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastHTTPAudioBroadcaster.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastHTTPAudioBroadcaster.java @@ -32,6 +32,7 @@ import org.apache.mina.core.service.IoHandlerAdapter; import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.ProtocolDecoderException; +import org.apache.mina.filter.ssl.SslFilter; import org.apache.mina.http.HttpClientCodec; import org.apache.mina.http.HttpRequestImpl; import org.apache.mina.http.api.DefaultHttpResponse; @@ -41,8 +42,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.net.ssl.SSLContext; import java.net.ConnectException; import java.net.InetSocketAddress; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -117,54 +122,60 @@ private boolean connect() // mSocketConnector.getFilterChain().addLast("logger", // new LoggingFilter(IcecastHTTPAudioBroadcaster.class)); + if(getConfiguration().isTlsEnabled()){ + try { + SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); + sslContext.init(null, null, new SecureRandom()); + SslFilter sslFilter = new SslFilter(sslContext); + mSocketConnector.getFilterChain().addFirst("sslFilter", sslFilter); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + mLog.error("Unable to build TLS Filter TLS", e); + } + } + mSocketConnector.getFilterChain().addLast("codec", new HttpClientCodec()); mSocketConnector.setHandler(new IcecastHTTPIOHandler()); } mStreamingSession = null; - Runnable runnable = new Runnable() - { - @Override - public void run() + Runnable runnable = () -> { + setBroadcastState(BroadcastState.CONNECTING); + + try + { + ConnectFuture future = mSocketConnector + .connect(new InetSocketAddress(getBroadcastConfiguration().getHost(), + getBroadcastConfiguration().getPort())); + future.awaitUninterruptibly(); + mStreamingSession = future.getSession(); + } + catch(RuntimeIoException rie) { - setBroadcastState(BroadcastState.CONNECTING); + Throwable throwableCause = rie.getCause(); - try + if(throwableCause instanceof ConnectException) { - ConnectFuture future = mSocketConnector - .connect(new InetSocketAddress(getBroadcastConfiguration().getHost(), - getBroadcastConfiguration().getPort())); - future.awaitUninterruptibly(); - mStreamingSession = future.getSession(); + setBroadcastState(BroadcastState.NO_SERVER); } - catch(RuntimeIoException rie) + else if(throwableCause != null) { - Throwable throwableCause = rie.getCause(); - - if(throwableCause instanceof ConnectException) - { - setBroadcastState(BroadcastState.NO_SERVER); - } - else if(throwableCause != null) - { - setBroadcastState(BroadcastState.DISCONNECTED); - mLog.debug("Failed to connect", rie); - } - else - { - setBroadcastState(BroadcastState.DISCONNECTED); - mLog.debug("Failed to connect - no exception is available"); - } - - disconnect(); + setBroadcastState(BroadcastState.DISCONNECTED); + mLog.debug("Failed to connect", rie); + } + else + { + setBroadcastState(BroadcastState.DISCONNECTED); + mLog.debug("Failed to connect - no exception is available"); } - mConnecting.set(false); + disconnect(); } + + mConnecting.set(false); }; - ThreadPool.SCHEDULED.schedule(runnable, 0l, TimeUnit.SECONDS); + ThreadPool.SCHEDULED.schedule(runnable, 0L, TimeUnit.SECONDS); } @@ -189,7 +200,7 @@ public void disconnect() public class IcecastHTTPIOHandler extends IoHandlerAdapter { @Override - public void sessionOpened(IoSession session) throws Exception + public void sessionOpened(IoSession session) { //Send stream configuration and user credentials upon connecting to remote server HttpRequestImpl request = new HttpRequestImpl(HttpVersion.HTTP_1_1, HttpMethod.PUT, @@ -264,7 +275,7 @@ public void sessionClosed(IoSession session) throws Exception } @Override - public void exceptionCaught(IoSession session, Throwable throwable) throws Exception + public void exceptionCaught(IoSession session, Throwable throwable) { if(throwable instanceof ProtocolDecoderException) { @@ -299,7 +310,7 @@ public void exceptionCaught(IoSession session, Throwable throwable) throws Excep } @Override - public void messageReceived(IoSession session, Object object) throws Exception + public void messageReceived(IoSession session, Object object) { if(object instanceof DefaultHttpResponse) { diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastHTTPConfiguration.java b/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastHTTPConfiguration.java index 6599d96e3..2b84059c0 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastHTTPConfiguration.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastHTTPConfiguration.java @@ -58,6 +58,7 @@ public BroadcastConfiguration copyOf() copy.setPassword(getPassword()); copy.setDelay(getDelay()); copy.setEnabled(false); + copy.setTlsStatus(isTlsEnabled()); //Icecast Configuration Parameters copy.setUserName(getUserName()); diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastHeader.java b/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastHeader.java index 8d6275f89..852790b16 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastHeader.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastHeader.java @@ -38,7 +38,7 @@ public enum IcecastHeader private String mValue; - private IcecastHeader(String value) + IcecastHeader(String value) { mValue = value; } diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastTCPAudioBroadcaster.java b/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastTCPAudioBroadcaster.java index fe4967626..8635ce046 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastTCPAudioBroadcaster.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastTCPAudioBroadcaster.java @@ -29,14 +29,19 @@ import org.apache.mina.core.service.IoHandlerAdapter; import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.ProtocolCodecFilter; +import org.apache.mina.filter.ssl.SslFilter; import org.apache.mina.transport.socket.nio.NioSocketConnector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.net.ssl.SSLContext; import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketException; import java.nio.channels.UnresolvedAddressException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -108,6 +113,17 @@ private boolean connect() // loggingFilter.setMessageSentLogLevel(LogLevel.NONE); // mSocketConnector.getFilterChain().addLast("logger", loggingFilter); + if(getConfiguration().isTlsEnabled()){ + try { + SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); + sslContext.init(null, null, new SecureRandom()); + SslFilter sslFilter = new SslFilter(sslContext); + mSocketConnector.getFilterChain().addFirst("sslFilter", sslFilter); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + mLog.error("Unable to build TLS Filter TLS", e); + } + } + mSocketConnector.getFilterChain().addLast("codec", new ProtocolCodecFilter(new IcecastCodecFactory())); mSocketConnector.setHandler(new IcecastTCPIOHandler()); @@ -115,59 +131,54 @@ private boolean connect() mStreamingSession = null; - Runnable runnable = new Runnable() - { - @Override - public void run() + Runnable runnable = () -> { + setBroadcastState(BroadcastState.CONNECTING); + + try { - setBroadcastState(BroadcastState.CONNECTING); + ConnectFuture future = mSocketConnector + .connect(new InetSocketAddress(getBroadcastConfiguration().getHost(), + getBroadcastConfiguration().getPort())); - try - { - ConnectFuture future = mSocketConnector - .connect(new InetSocketAddress(getBroadcastConfiguration().getHost(), - getBroadcastConfiguration().getPort())); - - boolean connected = future.await(CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS); - - if(connected) - { - mStreamingSession = future.getSession(); - mConnecting.set(false); - return; - } - } - catch(RuntimeIoException rioe) + boolean connected = future.await(CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS); + + if(connected) { - if(rioe.getCause() instanceof SocketException) - { - setBroadcastState(BroadcastState.NETWORK_UNAVAILABLE); - mConnecting.set(false); - return; - } + mStreamingSession = future.getSession(); + mConnecting.set(false); + return; } - catch(UnresolvedAddressException uae) + } + catch(RuntimeIoException rioe) + { + if(rioe.getCause() instanceof SocketException) { setBroadcastState(BroadcastState.NETWORK_UNAVAILABLE); mConnecting.set(false); return; } - catch(Exception e) - { - mLog.error("Error", e); - //Disregard ... we'll disconnect and try again - } - catch(Throwable t) - { - mLog.error("Throwable error caught", t); - } - - disconnect(); + } + catch(UnresolvedAddressException uae) + { + setBroadcastState(BroadcastState.NETWORK_UNAVAILABLE); mConnecting.set(false); + return; + } + catch(Exception e) + { + mLog.error("Error", e); + //Disregard ... we'll disconnect and try again } + catch(Throwable t) + { + mLog.error("Throwable error caught", t); + } + + disconnect(); + mConnecting.set(false); }; - ThreadPool.SCHEDULED.schedule(runnable, 0l, TimeUnit.SECONDS); + ThreadPool.SCHEDULED.schedule(runnable, 0L, TimeUnit.SECONDS); } return connected(); @@ -199,7 +210,7 @@ public class IcecastTCPIOHandler extends IoHandlerAdapter * Sends stream configuration and user credentials upon connecting to remote server */ @Override - public void sessionOpened(IoSession session) throws Exception + public void sessionOpened(IoSession session) { StringBuilder sb = new StringBuilder(); sb.append("SOURCE ").append(getConfiguration().getMountPoint()); @@ -259,7 +270,7 @@ public void sessionClosed(IoSession session) throws Exception } @Override - public void exceptionCaught(IoSession session, Throwable cause) throws Exception + public void exceptionCaught(IoSession session, Throwable cause) { if(!(cause instanceof IOException)) { @@ -270,13 +281,13 @@ public void exceptionCaught(IoSession session, Throwable cause) throws Exception } @Override - public void messageReceived(IoSession session, Object object) throws Exception + public void messageReceived(IoSession session, Object object) { if(object instanceof String) { String message = (String) object; - if(message != null && !message.trim().isEmpty()) + if(!message.trim().isEmpty()) { if(message.startsWith("HTTP/1.0 200 OK")) { @@ -308,8 +319,8 @@ else if(message.contains("HTTP/1.1 501")) } else { - mLog.error("[" + getStreamName() + "]Icecast TCP broadcaster - unrecognized message [ " + object.getClass() + - "] received:" + object.toString()); + mLog.error("[{}]Icecast TCP broadcaster - unrecognized message [{}] received. {}", + getStreamName(), object.getClass(), object.toString()); } } } diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastTCPConfiguration.java b/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastTCPConfiguration.java index 19cd9fdce..f9e187b8c 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastTCPConfiguration.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastTCPConfiguration.java @@ -66,6 +66,7 @@ public BroadcastConfiguration copyOf() copy.setPassword(getPassword()); copy.setDelay(getDelay()); copy.setEnabled(false); + copy.setTlsStatus(isTlsEnabled()); //Icecast Configuration Parameters copy.setUserName(getUserName()); diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v1/ShoutcastMetadata.java b/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v1/ShoutcastMetadata.java index ed41a6f9c..62dbf65a8 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v1/ShoutcastMetadata.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v1/ShoutcastMetadata.java @@ -45,7 +45,7 @@ public enum ShoutcastMetadata private String mTag; - private ShoutcastMetadata(String tag) + ShoutcastMetadata(String tag) { mTag = tag; } @@ -89,7 +89,7 @@ public String encode(BroadcastFormat broadcastFormat) public String encode(int value) { StringBuilder sb = new StringBuilder(); - sb.append(mTag).append(String.valueOf(value)).append(COMMAND_TERMINATOR); + sb.append(mTag).append(value).append(COMMAND_TERMINATOR); return sb.toString(); } } diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v1/ShoutcastV1AudioBroadcaster.java b/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v1/ShoutcastV1AudioBroadcaster.java index 996ef24be..fc3ca47bd 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v1/ShoutcastV1AudioBroadcaster.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v1/ShoutcastV1AudioBroadcaster.java @@ -30,13 +30,18 @@ import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.ProtocolCodecFilter; import org.apache.mina.filter.codec.textline.TextLineCodecFactory; +import org.apache.mina.filter.ssl.SslFilter; import org.apache.mina.transport.socket.nio.NioSocketConnector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.net.ssl.SSLContext; import java.io.IOException; import java.net.ConnectException; import java.net.InetSocketAddress; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -126,6 +131,17 @@ private boolean connect() // mSocketConnector.getFilterChain().addLast("logger", // new LoggingFilter(ShoutcastV1AudioBroadcaster.class)); + if(getConfiguration().isTlsEnabled()){ + try { + SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); + sslContext.init(null, null, new SecureRandom()); + SslFilter sslFilter = new SslFilter(sslContext); + mSocketConnector.getFilterChain().addFirst("sslFilter", sslFilter); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + mLog.error("Unable to build TLS Filter TLS", e); + } + } + mSocketConnector.getFilterChain().addLast("codec", new ProtocolCodecFilter(new TextLineCodecFactory())); @@ -134,52 +150,47 @@ private boolean connect() mStreamingSession = null; - Runnable runnable = new Runnable() - { - @Override - public void run() + Runnable runnable = () -> { + setBroadcastState(BroadcastState.CONNECTING); + + try { - setBroadcastState(BroadcastState.CONNECTING); + ConnectFuture future = mSocketConnector + .connect(new InetSocketAddress(getBroadcastConfiguration().getHost(), + getBroadcastConfiguration().getPort())); + future.awaitUninterruptibly(); + mStreamingSession = future.getSession(); + } + catch(RuntimeIoException rie) + { + Throwable throwableCause = rie.getCause(); - try + if(throwableCause instanceof ConnectException) { - ConnectFuture future = mSocketConnector - .connect(new InetSocketAddress(getBroadcastConfiguration().getHost(), - getBroadcastConfiguration().getPort())); - future.awaitUninterruptibly(); - mStreamingSession = future.getSession(); + setBroadcastState(BroadcastState.NO_SERVER); } - catch(RuntimeIoException rie) + else if(throwableCause != null) { - Throwable throwableCause = rie.getCause(); - - if(throwableCause instanceof ConnectException) - { - setBroadcastState(BroadcastState.NO_SERVER); - } - else if(throwableCause != null) - { - setBroadcastState(BroadcastState.ERROR); - mLog.debug("Failed to connect", rie); - } - else - { - setBroadcastState(BroadcastState.ERROR); - mLog.debug("Failed to connect - no exception is available"); - } - - disconnect(); + setBroadcastState(BroadcastState.ERROR); + mLog.debug("Failed to connect", rie); } - catch(Throwable t) + else { - disconnect(); + setBroadcastState(BroadcastState.ERROR); + mLog.debug("Failed to connect - no exception is available"); } - mConnecting.set(false); + disconnect(); } + catch(Throwable t) + { + disconnect(); + } + + mConnecting.set(false); }; - ThreadPool.SCHEDULED.schedule(runnable, 0l, TimeUnit.SECONDS); + ThreadPool.SCHEDULED.schedule(runnable, 0L, TimeUnit.SECONDS); } return connected(); @@ -210,7 +221,7 @@ public class ShoutcastIOHandler extends IoHandlerAdapter * Sends stream configuration and user credentials upon connecting to remote server */ @Override - public void sessionOpened(IoSession session) throws Exception + public void sessionOpened(IoSession session) { StringBuilder sb = new StringBuilder(); @@ -248,7 +259,7 @@ public void sessionClosed(IoSession session) throws Exception } @Override - public void exceptionCaught(IoSession session, Throwable cause) throws Exception + public void exceptionCaught(IoSession session, Throwable cause) { if(cause instanceof IOException) { @@ -293,7 +304,7 @@ else if(reason.startsWith("Operation timed out")) } @Override - public void messageReceived(IoSession session, Object object) throws Exception + public void messageReceived(IoSession session, Object object) { if(object instanceof String) { diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v1/ShoutcastV1BroadcastMetadataUpdater.java b/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v1/ShoutcastV1BroadcastMetadataUpdater.java index ce80c7076..6fdd55bef 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v1/ShoutcastV1BroadcastMetadataUpdater.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v1/ShoutcastV1BroadcastMetadataUpdater.java @@ -48,6 +48,7 @@ import java.net.ConnectException; import java.net.InetSocketAddress; import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -59,7 +60,6 @@ public class ShoutcastV1BroadcastMetadataUpdater implements IBroadcastMetadataUpdater { private final static Logger mLog = LoggerFactory.getLogger(ShoutcastV1BroadcastMetadataUpdater.class); - private final static String UTF8 = "UTF-8"; private ShoutcastV1Configuration mShoutcastV1Configuration; private AliasModel mAliasModel; @@ -98,14 +98,14 @@ private NioSocketConnector getSocketConnector() mSocketConnector.setHandler(new IoHandlerAdapter() { @Override - public void exceptionCaught(IoSession session, Throwable cause) throws Exception + public void exceptionCaught(IoSession session, Throwable cause) { //Single-use session - close it after we receive an error session.closeNow(); } @Override - public void messageReceived(IoSession session, Object message) throws Exception + public void messageReceived(IoSession session, Object message) { //Single-use session - close it after we receive a response session.closeNow(); @@ -138,45 +138,40 @@ public void update(IdentifierCollection identifierCollection) if(updateRequest != null) { - ThreadPool.SCHEDULED.schedule(new Runnable() - { - @Override - public void run() + ThreadPool.SCHEDULED.schedule(() -> { + try { - try + ConnectFuture connectFuture = getSocketConnector() + .connect(new InetSocketAddress(mShoutcastV1Configuration.getHost(), + mShoutcastV1Configuration.getPort())); + connectFuture.awaitUninterruptibly(); + IoSession session = connectFuture.getSession(); + + if(session != null) { - ConnectFuture connectFuture = getSocketConnector() - .connect(new InetSocketAddress(mShoutcastV1Configuration.getHost(), - mShoutcastV1Configuration.getPort())); - connectFuture.awaitUninterruptibly(); - IoSession session = connectFuture.getSession(); + session.write(updateRequest); + } + } + catch(Exception e) + { + Throwable throwableCause = e.getCause(); - if(session != null) - { - session.write(updateRequest); - } + if(throwableCause instanceof ConnectException) + { + //Do nothing, the server is unavailable } - catch(Exception e) + else { - Throwable throwableCause = e.getCause(); - - if(throwableCause instanceof ConnectException) + if(!mStackTraceLoggingSuppressed) { - //Do nothing, the server is unavailable - } - else - { - if(!mStackTraceLoggingSuppressed) - { - mLog.error("Error sending metadata update. Future errors will " + - "be suppressed", e); + mLog.error("Error sending metadata update. Future errors will " + + "be suppressed", e); - mStackTraceLoggingSuppressed = true; - } + mStackTraceLoggingSuppressed = true; } } } - }, 0l, TimeUnit.SECONDS); + }, 0L, TimeUnit.SECONDS); } //Fetch next metadata update to send @@ -271,26 +266,14 @@ private String getSong(IdentifierCollection identifierCollection) */ private HttpRequest createUpdateRequest(String song) { - try - { - StringBuilder sb = new StringBuilder(); - sb.append("pass=").append(mShoutcastV1Configuration.getPassword()); - sb.append("&mode=updinfo"); - sb.append("&song=").append(URLEncoder.encode(song, UTF8)); + StringBuilder sb = new StringBuilder(); + sb.append("pass=").append(mShoutcastV1Configuration.getPassword()); + sb.append("&mode=updinfo"); + sb.append("&song=").append(URLEncoder.encode(song, StandardCharsets.UTF_8)); - Map headers = new HashMap<>(); + Map headers = new HashMap<>(); - HttpRequestImpl request = new HttpRequestImpl(HttpVersion.HTTP_1_0, HttpMethod.GET, "/admin.cgi", + return new HttpRequestImpl(HttpVersion.HTTP_1_0, HttpMethod.GET, "/admin.cgi", sb.toString(), headers); - - return request; - } - catch(UnsupportedEncodingException e) - { - //This should never happen - mLog.error("UTF-8 encoding is not supported - can't update song metadata"); - } - - return null; } } diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v1/ShoutcastV1Configuration.java b/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v1/ShoutcastV1Configuration.java index 90875cad8..0d5c0e809 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v1/ShoutcastV1Configuration.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v1/ShoutcastV1Configuration.java @@ -31,6 +31,7 @@ public class ShoutcastV1Configuration extends BroadcastConfiguration private boolean mPublic; private int mChannels = 1; private int mBitRate = 16; + private boolean mIsTlsStatus; //No-arg JAXB constructor public ShoutcastV1Configuration() @@ -56,6 +57,7 @@ public BroadcastConfiguration copyOf() copy.setPassword(getPassword()); copy.setDelay(getDelay()); copy.setEnabled(false); + copy.setTLSEnabled(isTlsEnabled()); //Icecast Configuration Parameters copy.setGenre(getGenre()); @@ -164,4 +166,15 @@ public void setBitRate(int bitRate) { mBitRate = bitRate; } + + @JacksonXmlProperty(isAttribute = true, localName = "tls_enabled") + public boolean isTlsEnabled() + { + return mIsTlsStatus; + } + + public void setTlsStatus(boolean tlsStatus){ + this.mIsTlsStatus = tlsStatus; + } + } diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v2/ShoutcastV2AudioStreamingBroadcaster.java b/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v2/ShoutcastV2AudioStreamingBroadcaster.java index 9dbf88dcf..c45d53e5c 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v2/ShoutcastV2AudioStreamingBroadcaster.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v2/ShoutcastV2AudioStreamingBroadcaster.java @@ -56,14 +56,19 @@ import org.apache.mina.core.service.IoHandlerAdapter; import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.ProtocolCodecFilter; +import org.apache.mina.filter.ssl.SslFilter; import org.apache.mina.transport.socket.nio.NioSocketConnector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.net.ssl.SSLContext; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.ConnectException; import java.net.InetSocketAddress; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; import java.util.concurrent.LinkedTransferQueue; @@ -172,6 +177,16 @@ private boolean connect() // mSocketConnector.getFilterChain().addLast("logger", // new LoggingFilter(ShoutcastV2AudioBroadcaster.class)); + if(getConfiguration().isTlsEnabled()){ + try { + SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); + sslContext.init(null, null, new SecureRandom()); + SslFilter sslFilter = new SslFilter(sslContext); + mSocketConnector.getFilterChain().addFirst("sslFilter", sslFilter); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + mLog.error("Unable to build TLS Filter TLS", e); + } + } mSocketConnector.getFilterChain().addLast("codec", new ProtocolCodecFilter(new UltravoxProtocolFactory())); @@ -180,52 +195,47 @@ private boolean connect() mStreamingSession = null; - Runnable runnable = new Runnable() - { - @Override - public void run() + Runnable runnable = () -> { + setBroadcastState(BroadcastState.CONNECTING); + + try { - setBroadcastState(BroadcastState.CONNECTING); + ConnectFuture future = mSocketConnector + .connect(new InetSocketAddress(getBroadcastConfiguration().getHost(), + getBroadcastConfiguration().getPort())); + future.awaitUninterruptibly(); + mStreamingSession = future.getSession(); + } + catch(RuntimeIoException rie) + { + Throwable throwableCause = rie.getCause(); - try + if(throwableCause instanceof ConnectException) { - ConnectFuture future = mSocketConnector - .connect(new InetSocketAddress(getBroadcastConfiguration().getHost(), - getBroadcastConfiguration().getPort())); - future.awaitUninterruptibly(); - mStreamingSession = future.getSession(); + setBroadcastState(BroadcastState.NO_SERVER); } - catch(RuntimeIoException rie) + else if(throwableCause != null) { - Throwable throwableCause = rie.getCause(); - - if(throwableCause instanceof ConnectException) - { - setBroadcastState(BroadcastState.NO_SERVER); - } - else if(throwableCause != null) - { - setBroadcastState(BroadcastState.ERROR); - mLog.error("Failed to connect", rie); - } - else - { - setBroadcastState(BroadcastState.ERROR); - mLog.error("Failed to connect - no exception is available"); - } - - disconnect(); + setBroadcastState(BroadcastState.ERROR); + mLog.error("Failed to connect", rie); } - catch(Throwable t) + else { - disconnect(); + setBroadcastState(BroadcastState.ERROR); + mLog.error("Failed to connect - no exception is available"); } - mConnecting.set(false); + disconnect(); + } + catch(Throwable t) + { + disconnect(); } + + mConnecting.set(false); }; - ThreadPool.SCHEDULED.schedule(runnable, 0l, TimeUnit.SECONDS); + ThreadPool.SCHEDULED.schedule(runnable, 0L, TimeUnit.SECONDS); } return connected(); @@ -406,7 +416,7 @@ public class ShoutcastV2IOHandler extends IoHandlerAdapter * Sends stream configuration and user credentials upon connecting to remote server */ @Override - public void sessionOpened(IoSession session) throws Exception + public void sessionOpened(IoSession session) { session.write(UltravoxMessageFactory.getMessage(UltravoxMessageType.REQUEST_CIPHER)); } @@ -429,7 +439,7 @@ public void sessionClosed(IoSession session) throws Exception } @Override - public void exceptionCaught(IoSession session, Throwable cause) throws Exception + public void exceptionCaught(IoSession session, Throwable cause) { if(cause instanceof IOException) { @@ -474,7 +484,7 @@ else if(reason.startsWith("Operation timed out")) } @Override - public void messageReceived(IoSession session, Object object) throws Exception + public void messageReceived(IoSession session, Object object) { if(object instanceof UltravoxMessage) { diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v2/ShoutcastV2Configuration.java b/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v2/ShoutcastV2Configuration.java index e32ffae1e..8d2e09742 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v2/ShoutcastV2Configuration.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v2/ShoutcastV2Configuration.java @@ -37,6 +37,7 @@ public class ShoutcastV2Configuration extends BroadcastConfiguration private String mGenre; private String mURL; private boolean mPublic = true; + private boolean mIsTlsStatus; public ShoutcastV2Configuration() { @@ -63,6 +64,7 @@ public BroadcastConfiguration copyOf() copy.setPassword(getPassword()); copy.setDelay(getDelay()); copy.setEnabled(false); + copy.setTLSEnabled(isTlsEnabled()); //Shoutcast V2 Configuration Parameters copy.setStreamID(getStreamID()); @@ -204,4 +206,15 @@ public boolean hasURL() { return mURL != null && !mURL.isEmpty(); } + + @JacksonXmlProperty(isAttribute = true, localName = "tls_enabled") + public boolean isTlsEnabled() + { + return mIsTlsStatus; + } + + public void setTlsStatus(boolean tlsStatus){ + this.mIsTlsStatus = tlsStatus; + } + } diff --git a/src/main/java/io/github/dsheirer/gui/playlist/streaming/AbstractBroadcastEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/streaming/AbstractBroadcastEditor.java index ed7fca8a8..821a1fceb 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/streaming/AbstractBroadcastEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/streaming/AbstractBroadcastEditor.java @@ -28,7 +28,6 @@ import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.geometry.Insets; -import javafx.scene.Node; import javafx.scene.control.Alert; import javafx.scene.control.Button; import javafx.scene.control.ButtonType; @@ -51,6 +50,7 @@ public abstract class AbstractBroadcastEditor private TextField mFormatField; private TextField mNameTextField; private ToggleSwitch mEnabledSwitch; + private ToggleSwitch mTLSSwitch; protected EditorModificationListener mEditorModificationListener = new EditorModificationListener(); /** @@ -89,16 +89,19 @@ public void setItem(T item) getNameTextField().setDisable(item == null); getEnabledSwitch().setDisable(item == null); + getTLSSwitch().setDisable(item == null); if(item != null) { getNameTextField().setText(item.getName()); getEnabledSwitch().selectedProperty().set(item.isEnabled()); + getTLSSwitch().selectedProperty().set(item.isTlsEnabled()); } else { getNameTextField().setText(null); getEnabledSwitch().selectedProperty().set(false); + getTLSSwitch().selectedProperty().set(false); } } @@ -109,6 +112,7 @@ public void save() if(configuration != null) { configuration.setEnabled(getEnabledSwitch().isSelected()); + configuration.setTLSEnabled(getTLSSwitch().isSelected()); //Detect stream name change so that we can update any aliases that might be using the previous name String previousName = configuration.getName(); @@ -125,13 +129,11 @@ public void save() alert.setTitle("Update Aliases"); alert.setHeaderText("Rename requires updating aliases for this stream"); alert.setContentText("Do you want to update aliases to new stream name?"); - alert.initOwner(((Node)getSaveButton()).getScene().getWindow()); + alert.initOwner((getSaveButton()).getScene().getWindow()); //Workaround for JavaFX KDE on Linux bug in FX 10/11: https://bugs.openjdk.java.net/browse/JDK-8179073 alert.setResizable(true); - alert.onShownProperty().addListener(e -> { - Platform.runLater(() -> alert.setResizable(false)); - }); + alert.onShownProperty().addListener(e -> Platform.runLater(() -> alert.setResizable(false))); alert.showAndWait().ifPresent(buttonType -> { if(buttonType == ButtonType.YES) @@ -214,6 +216,19 @@ protected ToggleSwitch getEnabledSwitch() return mEnabledSwitch; } + protected ToggleSwitch getTLSSwitch() + { + if(mTLSSwitch == null) + { + mTLSSwitch = new ToggleSwitch(); + mTLSSwitch.setDisable(true); + mTLSSwitch.selectedProperty() + .addListener((observable, oldValue, newValue) -> modifiedProperty().set(true)); + } + + return mTLSSwitch; + } + /** * Simple string change listener that sets the editor modified flag to true any time text fields are edited. */ diff --git a/src/main/java/io/github/dsheirer/gui/playlist/streaming/IcecastStreamEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/streaming/IcecastStreamEditor.java index 9d939f7f6..5aa5fba89 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/streaming/IcecastStreamEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/streaming/IcecastStreamEditor.java @@ -87,6 +87,7 @@ public void save() getItem().setDescription(getDescriptionTextField().getText()); getItem().setGenre(getGenreTextField().getText()); getItem().setURL(getURLTextField().getText()); + getItem().setTlsStatus(getTLSSwitch().isSelected()); } super.save(); @@ -142,6 +143,14 @@ protected GridPane getEditorPane() GridPane.setConstraints(getPortTextField(), 3, 2); mEditorPane.getChildren().add(getPortTextField()); + Label tlsLabel = new Label("SSL/TLS"); + GridPane.setHalignment(tlsLabel, HPos.RIGHT); + GridPane.setConstraints(tlsLabel, 4, 2); + mEditorPane.getChildren().add(tlsLabel); + + GridPane.setConstraints(getTLSSwitch(), 5, 2); + mEditorPane.getChildren().add(getTLSSwitch()); + Label mountPointLabel = new Label("Mount Point"); GridPane.setHalignment(mountPointLabel, HPos.RIGHT); GridPane.setConstraints(mountPointLabel, 0, 3); diff --git a/src/main/java/io/github/dsheirer/gui/playlist/streaming/ShoutcastV1StreamEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/streaming/ShoutcastV1StreamEditor.java index d99feeb2a..5eb13b96a 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/streaming/ShoutcastV1StreamEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/streaming/ShoutcastV1StreamEditor.java @@ -87,6 +87,7 @@ public void save() getItem().setDescription(getDescriptionTextField().getText()); getItem().setGenre(getGenreTextField().getText()); getItem().setPublic(getPublicToggleSwitch().isSelected()); + getItem().setTlsStatus(getTLSSwitch().isSelected()); } super.save(); @@ -150,6 +151,14 @@ protected GridPane getEditorPane() GridPane.setConstraints(getPortTextField(), 3, 2); mEditorPane.getChildren().add(getPortTextField()); + Label tlsLabel = new Label("SSL/TLS"); + GridPane.setHalignment(tlsLabel, HPos.RIGHT); + GridPane.setConstraints(tlsLabel, 4, 2); + mEditorPane.getChildren().add(tlsLabel); + + GridPane.setConstraints(getTLSSwitch(), 5, 2); + mEditorPane.getChildren().add(getTLSSwitch()); + Label passwordLabel = new Label("Password"); GridPane.setHalignment(passwordLabel, HPos.RIGHT); GridPane.setConstraints(passwordLabel, 0, 3); diff --git a/src/main/java/io/github/dsheirer/gui/playlist/streaming/ShoutcastV2StreamEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/streaming/ShoutcastV2StreamEditor.java index efd81738c..fc2b4fc95 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/streaming/ShoutcastV2StreamEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/streaming/ShoutcastV2StreamEditor.java @@ -98,6 +98,7 @@ public void save() getItem().setPublic(getPublicToggleSwitch().isSelected()); getItem().setUserID(getUserIdTextField().getText()); getItem().setStreamID(getStreamIdTextField().get()); + getItem().setTlsStatus(getTLSSwitch().isSelected()); } super.save(); @@ -161,6 +162,14 @@ protected GridPane getEditorPane() GridPane.setConstraints(getPortTextField(), 3, 2); mEditorPane.getChildren().add(getPortTextField()); + Label tlsLabel = new Label("SSL/TLS"); + GridPane.setHalignment(tlsLabel, HPos.RIGHT); + GridPane.setConstraints(tlsLabel, 4, 2); + mEditorPane.getChildren().add(tlsLabel); + + GridPane.setConstraints(getTLSSwitch(), 5, 2); + mEditorPane.getChildren().add(getTLSSwitch()); + Label userIdLabel = new Label("User ID"); GridPane.setHalignment(userIdLabel, HPos.RIGHT); GridPane.setConstraints(userIdLabel, 0, 3);