From 77133d1321af5626f0fc31daa5abc24cdbc62177 Mon Sep 17 00:00:00 2001 From: "Amara Graham (Keller)" Date: Fri, 2 Oct 2020 09:09:45 -0500 Subject: [PATCH 1/6] Switched blacklist to blocklist in text Links, code, and article title remain "blacklist" --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index db29bd6..dcc846e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# BlacklistStreams -The **BlacklistStreams** server listener and HTTP Provider for [Wowza Streaming Engine™ media server software](https://www.wowza.com/products/streaming-engine) enables you to persistently blacklist streams published to your Wowza media server. +# BlocklistStreams +The **BlocklistStreams** server listener and HTTP Provider for [Wowza Streaming Engine™ media server software](https://www.wowza.com/products/streaming-engine) enables you to persistently blocklist streams published to your Wowza media server. This repo includes a [compiled version](/lib/wse-plugin-blackliststreams.jar). @@ -7,22 +7,22 @@ This repo includes a [compiled version](/lib/wse-plugin-blackliststreams.jar). Wowza Streaming Engine 4.0.0 or later is required. ## Usage -**BlacklistStreams** provides the following functionality: +**BlocklistStreams** provides the following functionality: -* Blacklist stream names using an HTTP Provider (allows selection of streams to blacklist). -* Contents of blacklist persist through each Wowza Streaming Engine media server reboot. -* Allows previously blacklisted stream names to be whitelisted again. -* Supports blacklisting of both RTMP and RTSP sources. +* Blocklist stream names using an HTTP Provider (allows selection of streams to blocklist). +* Contents of blocklist persist through each Wowza Streaming Engine media server reboot. +* Allows previously blocklisted stream names to be whitelisted again. +* Supports blocklisting of both RTMP and RTSP sources. -To blacklist a stream: +To blocklist a stream: -1. Start your Wowza Streaming Engine media server, and then open the following URL to the Stream Blacklists page in a web browser:
http://[wowza-ip-address]:8086/blacklist
+1. Start your Wowza Streaming Engine media server, and then open the following URL to the Stream Blocklists page in a web browser:
http://[wowza-ip-address]:8086/blacklist
2. When prompted, enter the credentials that you use to sign in to Wowza Streaming Engine Manager. -3. Publish your first stream, and then in the **Stream Blacklists** page, click **Check for new streams**. Your new stream will be displayed in the **Published Streams** list. +3. Publish your first stream, and then in the **Stream Blocklists** page, click **Check for new streams**. Your new stream will be displayed in the **Published Streams** list. -4. Click the **Blacklist** link next to the published stream to add that stream to the blacklist. This will also stop the stream. Any subsequent attempts to publish to that [app-name]/[app-instance]/[stream-name] sequence are rejected until you remove the stream from the blacklist. +4. Click the **Blocklist** link next to the published stream to add that stream to the blocklist. This will also stop the stream. Any subsequent attempts to publish to that [app-name]/[app-instance]/[stream-name] sequence are rejected until you remove the stream from the blocklist. ## More resources To use the compiled version of this module, see [Blacklist a stream in session with a Wowza Streaming Engine server listener and HTTP provider](https://www.wowza.com/docs/how-to-blacklist-a-stream-in-session-blackliststreams). From ca046aa809a304705c81c5f89980cf46afcc25af Mon Sep 17 00:00:00 2001 From: Amara Date: Fri, 2 Oct 2020 11:49:21 -0500 Subject: [PATCH 2/6] replaced code and inline comments --- .../wms/plugin/blacklist/BlockListUtils.java | 212 +++++++++ .../HTTPProviderBlocklistStreams.java | 402 ++++++++++++++++++ .../ServerListenerBlocklistStreams.java | 382 +++++++++++++++++ 3 files changed, 996 insertions(+) create mode 100644 src/com/wowza/wms/plugin/blacklist/BlockListUtils.java create mode 100644 src/com/wowza/wms/plugin/blacklist/HTTPProviderBlocklistStreams.java create mode 100644 src/com/wowza/wms/plugin/blacklist/ServerListenerBlocklistStreams.java diff --git a/src/com/wowza/wms/plugin/blacklist/BlockListUtils.java b/src/com/wowza/wms/plugin/blacklist/BlockListUtils.java new file mode 100644 index 0000000..bb7cb74 --- /dev/null +++ b/src/com/wowza/wms/plugin/blacklist/BlockListUtils.java @@ -0,0 +1,212 @@ +/* + * This code and all components (c) Copyright 2006 - 2018, Wowza Media Systems, LLC. All rights reserved. + * This code is licensed pursuant to the Wowza Public License version 1.0, available at www.wowza.com/legal. + */ +package com.wowza.wms.plugin.blocklist; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; + +import com.wowza.wms.bootstrap.Bootstrap; +import com.wowza.wms.logging.WMSLogger; +import com.wowza.wms.logging.WMSLoggerFactory; + +public class BlockListUtils +{ + private static ArrayList streamblockList = new ArrayList(); + private static String blocklistConfigPath = Bootstrap.getServerHome(Bootstrap.CONFIGHOME) + "/conf/blocklist.txt"; + static String separatorChar = ":"; + private static WMSLogger logger = WMSLoggerFactory.getLogger(BlockListUtils.class); + + public static void setConfigPath(String config) + { + BlockListUtils.blocklistConfigPath = config; + } + + public static void setSeparatorChar(String separatorChar) + { + BlockListUtils.separatorChar = separatorChar; + } + + public static void blockListStream(String application, String appInstance, String streamName) + { + BlockListUtils.mergeData(); + + synchronized(BlockListUtils.streamblockList) + { + String streamKey = application + separatorChar + appInstance + separatorChar + streamName; + if (!BlockListUtils.streamblockList.contains(streamKey)) + { + BlockListUtils.streamblockList.add(streamKey); + BlockListUtils.printContents(); + BlockListUtils.saveFile(); + } + } + } + + public static void removeStreamFromList(String application, String appInstance, String streamName) + { + BlockListUtils.mergeData(); + String streamKey = application + separatorChar + appInstance + separatorChar + streamName; + if (ServerListenerBlocklistStreams.debug) + logger.info(ServerListenerBlocklistStreams.MODULE_NAME + ".removeStreamFromList[" + streamKey + "] Stream is initiated "); + + synchronized(BlockListUtils.streamblockList) + { + if (BlockListUtils.streamblockList.contains(streamKey)) + { + if (BlockListUtils.streamblockList.remove(streamKey)) + { + BlockListUtils.saveFile(); + BlockListUtils.printContents(); + + logger.info(ServerListenerBlocklistStreams.MODULE_NAME + ".removeStreamFromList[" + streamKey + "] Completed "); + } + else + { + logger.info(ServerListenerBlocklistStreams.MODULE_NAME + ".removeStreamFromList[" + streamKey + "] Does not exist-2 "); + } + } + else + { + logger.info(ServerListenerBlocklistStreams.MODULE_NAME + ".removeStreamFromList[" + streamKey + "] Does not exist "); + } + } + } + + public static boolean isStreamblockListed(String application, String appInstance, String streamName) + { + BlockListUtils.mergeData(); + String streamKey = application + separatorChar + appInstance + separatorChar + streamName; + return BlockListUtils.streamblockList.contains(streamKey); + } + + public static ArrayList getblockListedStreams() + { + BlockListUtils.mergeData(); + synchronized(BlockListUtils.streamblockList) + { + return BlockListUtils.streamblockList; + } + } + + public static void mergeData() + { + synchronized(BlockListUtils.streamblockList) + { + ArrayList fileStored = BlockListUtils.getStoredblocklistItems(); + if (fileStored.size() > 0) + { + for (int i = 0; i < fileStored.size(); i++) + { + if (!BlockListUtils.streamblockList.contains(fileStored.get(i))) + { + BlockListUtils.streamblockList.add(fileStored.get(i)); + } + } + } + } + } + + private static ArrayList getStoredblocklistItems() + { + ArrayList blockList = new ArrayList(); + File tmpFile = new File(BlockListUtils.blocklistConfigPath); + if (tmpFile.exists()) + { + FileInputStream in = null; + BufferedReader br = null; + try + { + in = new FileInputStream(BlockListUtils.blocklistConfigPath); + br = new BufferedReader(new InputStreamReader(in)); + String cacheItem; + + while ((cacheItem = br.readLine()) != null) + { + cacheItem = cacheItem.trim(); + if (!cacheItem.startsWith("#") && cacheItem.length() > 0) + { + blockList.add(cacheItem); + } + } + } + catch (Exception e) + { + logger.error(ServerListenerBlocklistStreams.MODULE_NAME + ".getStoredblocklistItems() ", e); + } + finally + { + try + { + if (br != null) + br.close(); + } + catch (IOException e) + { + } + br = null; + try + { + if (in != null) + in.close(); + } + catch (IOException e) + { + } + in = null; + } + } + else + { + logger.info(ServerListenerBlocklistStreams.MODULE_NAME + ".getStoredblocklistItems could not find block list items list"); + } + return blockList; + } + + private static void saveFile() + { + synchronized(BlockListUtils.streamblockList) + { + ArrayList bli = BlockListUtils.streamblockList; + try + { + File f = new File(blocklistConfigPath); + if (!f.exists()) + { + f.createNewFile(); + } + + FileWriter tmpFile = new FileWriter(blocklistConfigPath, false); + StringBuilder b = new StringBuilder(); + for (int i = 0; i < bli.size(); i++) + { + logger.info(ServerListenerBlocklistStreams.MODULE_NAME + ".saveFile " + "Line: " + bli.get(i)); + b.append(bli.get(i) + "\n"); + } + tmpFile.write(b.toString()); + tmpFile.close(); + } + catch (IOException ioe) + { + logger.error(ServerListenerBlocklistStreams.MODULE_NAME + ".saveFile " + "IOException: " + ioe.getMessage(), ioe); + } + } + } + + private static void printContents() + { + ArrayList bli = BlockListUtils.getblockListedStreams(); + for (int i = 0; i < bli.size(); i++) + { + logger.info(ServerListenerBlocklistStreams.MODULE_NAME + ".printContents " + "bli.get(i): " + bli.get(i)); + } + + } + +} diff --git a/src/com/wowza/wms/plugin/blacklist/HTTPProviderBlocklistStreams.java b/src/com/wowza/wms/plugin/blacklist/HTTPProviderBlocklistStreams.java new file mode 100644 index 0000000..948b601 --- /dev/null +++ b/src/com/wowza/wms/plugin/blacklist/HTTPProviderBlocklistStreams.java @@ -0,0 +1,402 @@ +/* + * This code and all components (c) Copyright 2006 - 2018, Wowza Media Systems, LLC. All rights reserved. + * This code is licensed pursuant to the Wowza Public License version 1.0, available at www.wowza.com/legal. + */ +package com.wowza.wms.plugin.blocklist; + +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.wowza.wms.application.IApplication; +import com.wowza.wms.application.IApplicationInstance; +import com.wowza.wms.client.IClient; +import com.wowza.wms.http.HTTProvider2Base; +import com.wowza.wms.http.IHTTPRequest; +import com.wowza.wms.http.IHTTPResponse; +import com.wowza.wms.httpstreamer.model.IHTTPStreamerSession; +import com.wowza.wms.logging.WMSLogger; +import com.wowza.wms.logging.WMSLoggerFactory; +import com.wowza.wms.mediacaster.MediaCasterItem; +import com.wowza.wms.mediacaster.MediaCasterStreamId; +import com.wowza.wms.mediacaster.wowza.LiveMediaStreamURL; +import com.wowza.wms.rtp.model.RTPSession; +import com.wowza.wms.rtp.model.RTPStream; +import com.wowza.wms.stream.IMediaStream; +import com.wowza.wms.vhost.IVHost; + +public class HTTPProviderBlocklistStreams extends HTTProvider2Base +{ + public static final String MODULE_NAME = "HTTPProviderblocklistStreams"; + + private WMSLogger logger = WMSLoggerFactory.getLogger(getClass()); + + public void onHTTPRequest(IVHost vhost, IHTTPRequest req, IHTTPResponse resp) + { + if (!doHTTPAuthentication(vhost, req, resp)) + return; + + String postResponse = this.handlePost(req, vhost); + + String html = ""; + + // get blocklisted streams + html += this.getblockListedStreams(); + + // get published streams + html += this.getPublishedStreams(vhost); + + //refresh button + html += this.getRefresh(); + + String postMsg = ""; + if (postResponse != null) + { + postMsg = this.getMsgHeader(postResponse, req); + } + String retStr = this.getHtmlPage(html, postMsg); + try + { + OutputStream out = resp.getOutputStream(); + byte[] outBytes = retStr.getBytes(); + out.write(outBytes); + } + catch (Exception e) + { + logger.error(MODULE_NAME + ".onHTTPRequest()", e); + } + } + + private String getRefresh() + { +// String html = ""; + String html = "

"; + return html; + } + + private String getblockListedStreams() + { + String html = ""; + ArrayList streams = BlockListUtils.getblockListedStreams(); + if (streams.size() > 0) + { + + html += this.divHeader("blocklisted") + this.divSectionStart(); + html += ""; + for (int i = 0; i < streams.size(); i++) + { + String[] parts = streams.get(i).split(BlockListUtils.separatorChar); + if (parts.length == 3) + { + String applicationName = parts[0]; + String appInstanceName = parts[1]; + String streamName = parts[2]; + html += this.addRow(applicationName, appInstanceName, streamName); + } + } + html += "
"; + html += this.divSectionEnd(); + } + + return html; + } + + private String getPublishedStreams(IVHost vhost) + { + String html = ""; + if (vhost != null) + { + html += this.divHeader("Published Streams"); + html += this.divSectionStart(); + html += this.startSection(vhost.getName()); + List appNames = vhost.getApplicationNames(); + if (appNames.size() > 0) + { + Iterator appNameIterator = appNames.iterator(); + while (appNameIterator.hasNext()) + { + String applicationName = appNameIterator.next(); + IApplication application = vhost.getApplication(applicationName); + + List appInstances = application.getAppInstanceNames(); + if (appInstances.size() > 0) + { + Iterator iterAppInstances = appInstances.iterator(); + while (iterAppInstances.hasNext()) + { + String appInstanceName = iterAppInstances.next(); + IApplicationInstance appInstance = application.getAppInstance(appInstanceName); + + Set names = new HashSet(); + List publishedStreams = appInstance.getPublishStreamNames(); + if (publishedStreams.size() > 0) + { + Iterator publishedStreamIterator = publishedStreams.iterator(); + while (publishedStreamIterator.hasNext()) + { + String streamName = publishedStreamIterator.next(); + MediaCasterStreamId mediaCasterStreamId = MediaCasterItem.parseIdString(streamName); + streamName = mediaCasterStreamId.getName(); + if (streamName.indexOf("://") >= 0) + { + String resolvedName = LiveMediaStreamURL.decodeURLToStreamName(streamName); + if (resolvedName != null) + streamName = resolvedName; + } + names.add(streamName); + } + } + for (String name : names) + { + if (!BlockListUtils.isStreamblockListed(applicationName, appInstanceName, name)) + { + html += this.addRow(applicationName, appInstanceName, name); + } + } + } + } + } + } + else + { + html += this.addEmptyRow("No streams published.."); + } + html += this.endSection(); + html += this.divSectionEnd(); + } + return html; + } + + private boolean blocklistStream(IVHost vhost, String applicationName, String appInstanceName, String streamName) + { + try + { + IApplicationInstance appInstance = vhost.getApplication(applicationName).getAppInstance(appInstanceName); + if (appInstance != null) + { + + // force client disconnect + List rtmpClients = appInstance.getClients(); + List httpClients = appInstance.getHTTPStreamerSessions(); + List rtpClients = appInstance.getRTPSessions(); + + // http + Iterator httpClientIter = httpClients.iterator(); + IHTTPStreamerSession httpClient; + while (httpClientIter.hasNext()) + { + try + { + httpClient = httpClientIter.next(); + if (httpClient == null) + continue; + + if (streamName.equals(httpClient.getStreamName())) + { + httpClient.rejectSession(); + } + } + catch (Exception e) + { + logger.error(MODULE_NAME + ".blocklistStream()", e); + } + + } + + //rtmp + Iterator rtmpClientIter = rtmpClients.iterator(); + IClient rtmpClient; + while (rtmpClientIter.hasNext()) + { + try + { + rtmpClient = rtmpClientIter.next(); + if (rtmpClient == null) + continue; + + List Names = rtmpClient.getPlayStreams(); + + if (Names.size() > 0) + { + Iterator iter = Names.iterator(); + while (iter.hasNext()) + { + IMediaStream stream = (IMediaStream)iter.next(); + + String name = stream.getName(); + if (name.indexOf("://") >= 0) + { + String resolvedName = LiveMediaStreamURL.decodeURLToStreamName(name); + if (resolvedName != null) + name = resolvedName; + } + + if (streamName.equals(name)) + { + rtmpClient.setShutdownClient(true); + } + } + } + } + catch (Exception e) + { + logger.error(MODULE_NAME + ".blocklistStream()", e); + } + } + + //rtp + Iterator rtpClientIter = rtpClients.iterator(); + RTPSession rtpClient; + while (rtpClientIter.hasNext()) + { + try + { + rtpClient = rtpClientIter.next(); + if (rtpClient == null) + continue; + + if (streamName.equals(rtpClient.getRTSPStream().getStreamName())) + { + appInstance.getVHost().getRTPContext().shutdownRTPSession(rtpClient); + } + } + catch (Exception e) + { + logger.error(MODULE_NAME + ".blocklistStream()", e); + } + } + } + + //shutdown stream + IMediaStream publishedStream = appInstance.getStreams().getStream(streamName); + if (publishedStream != null) + { + IClient client = publishedStream.getClient(); + if (client != null) + { + client.setShutdownClient(true); + } + else + { + RTPStream rtpStream = publishedStream.getRTPStream(); + if (rtpStream != null) + { + appInstance.getVHost().getRTPContext().shutdownRTPSession(rtpStream.getSession()); + } + } + } + return true; + } + catch (Exception ex) + { + logger.error(MODULE_NAME + ".blocklistStream()", ex); + } + return false; + } + + private String handlePost(IHTTPRequest req, IVHost vhost) + { + if (req.getMethod().equalsIgnoreCase("post")) + { + req.parseBodyForParams(true); + } + + Map> params = req.getParameterMap(); + System.out.println(params.toString()); + if (params.containsKey("blocklist")) + { + String appPath = params.get("application").get(0) + "/" + params.get("appInstance").get(0) + "/" + params.get("stream").get(0); + if (params.get("blocklist").get(0).equalsIgnoreCase("1")) + { + BlockListUtils.blockListStream(params.get("application").get(0), params.get("appInstance").get(0), params.get("stream").get(0)); + if (this.blocklistStream(vhost, params.get("application").get(0), params.get("appInstance").get(0), params.get("stream").get(0))) + { +// blockListed.removeStreamFromList(params.get("application").get(0), params.get("appInstance").get(0), params.get("stream").get(0)); + return appPath + " has been added to the blocklist"; + } + return appPath + " failed to add to blocklist."; + } + else + { + BlockListUtils.removeStreamFromList(params.get("application").get(0), params.get("appInstance").get(0), params.get("stream").get(0)); + return appPath + " has been removed from the blocklist"; + } + } + return null; + } + + /* + * HTML Helpers + */ + + private String divSectionStart() + { + return "
"; + } + + private String divSectionEnd() + { + return "
"; + } + + private String divHeader(String title) + { + return "
" + title + "
"; + } + + private String getMsgHeader(String msg, IHTTPRequest req) + { + if (!msg.isEmpty()) + { + String requestURL = req.getRequestURL(); + String[] parts = requestURL.split("\\/"); + String filterName = parts[parts.length - 1]; + msg += " clear"; + } + return "
" + msg + "
"; + } + + private String getHtmlPage(String body, String msg) + { + return "\n" + "\n" + "blocklist Streams\n" + "\n" + " \n" + " \n

Stream blocklists

" + "
" + " " + msg + " " + body + " " + + "
" + " \n" + " " + + " " + " " + "
" + "
" + " " + ""; + } + + private String startSection(String vhost) + { + return "
" + vhost + "
" + + " " + " " + " " + " " + ""; + } + + private String endSection() + { + return "
ApplicationAppInstanceStreamAction
"; + } + + private String addEmptyRow(String msg) + { + return "" + msg + ""; + } + + private String addRow(String appName, String appInstance, String streamName) + { + + String blocklist = "1"; + String blocklistedTitle = "blocklist"; + if (BlockListUtils.isStreamblockListed(appName, appInstance, streamName)) + { + blocklistedTitle = "Whitelist"; + blocklist = "0"; + } + return "" + " " + appName + "" + " " + appInstance + "" + + " " + streamName + "" + " " + + blocklistedTitle + " " + ""; + } +} diff --git a/src/com/wowza/wms/plugin/blacklist/ServerListenerBlocklistStreams.java b/src/com/wowza/wms/plugin/blacklist/ServerListenerBlocklistStreams.java new file mode 100644 index 0000000..ed101d2 --- /dev/null +++ b/src/com/wowza/wms/plugin/blacklist/ServerListenerBlocklistStreams.java @@ -0,0 +1,382 @@ +/* + * This code and all components (c) Copyright 2006 - 2018, Wowza Media Systems, LLC. All rights reserved. + * This code is licensed pursuant to the Wowza Public License version 1.0, available at www.wowza.com/legal. + */ +package com.wowza.wms.plugin.blocklist; + +import com.wowza.util.StringUtils; +import com.wowza.wms.amf.AMFDataList; +import com.wowza.wms.amf.AMFPacket; +import com.wowza.wms.application.IApplication; +import com.wowza.wms.application.IApplicationInstance; +import com.wowza.wms.application.IApplicationInstanceNotify; +import com.wowza.wms.application.IApplicationNotify; +import com.wowza.wms.client.IClient; +import com.wowza.wms.httpstreamer.model.IHTTPStreamerSession; +import com.wowza.wms.logging.WMSLogger; +import com.wowza.wms.logging.WMSLoggerFactory; +import com.wowza.wms.mediacaster.IMediaCaster; +import com.wowza.wms.module.ModuleBase; +import com.wowza.wms.module.ModuleCore; +import com.wowza.wms.request.RequestFunction; +import com.wowza.wms.rtp.model.RTPRequestStatus; +import com.wowza.wms.rtp.model.RTPSession; +import com.wowza.wms.rtp.model.RTPStream; +import com.wowza.wms.server.IServer; +import com.wowza.wms.server.IServerNotify2; +import com.wowza.wms.stream.IMediaStream; +import com.wowza.wms.stream.IMediaStreamActionNotify; +import com.wowza.wms.stream.IMediaStreamActionNotify2; +import com.wowza.wms.stream.IMediaStreamNameAliasProvider; +import com.wowza.wms.stream.IMediaStreamNameAliasProvider2; +import com.wowza.wms.stream.IMediaStreamNotify; +import com.wowza.wms.stream.livepacketizer.ILiveStreamPacketizer; +import com.wowza.wms.util.MediaCasterUtils; +import com.wowza.wms.vhost.IVHost; +import com.wowza.wms.vhost.IVHostNotify; +import com.wowza.wms.vhost.VHostSingleton; + +public class ServerListenerBlocklistStreams extends ModuleBase implements IServerNotify2 +{ + private class VHostNotifier implements IVHostNotify + { + private final ApplicationNotify listener = new ApplicationNotify(); + + @Override + public void onVHostCreate(IVHost vhost) + { + vhost.addApplicationListener(listener); + } + + @Override + public void onVHostInit(IVHost vhost) + { + } + + @Override + public void onVHostShutdownStart(IVHost vhost) + { + } + + @Override + public void onVHostShutdownComplete(IVHost vhost) + { + vhost.removeApplicationListener(listener); + } + + @Override + public void onVHostClientConnect(IVHost vhost, IClient inClient, RequestFunction function, AMFDataList params) + { + } + + } + + class ApplicationNotify implements IApplicationNotify + { + private final ApplicationInstanceNotify listener = new ApplicationInstanceNotify(); + + public void onApplicationCreate(IApplication application) + { + application.addApplicationInstanceListener(listener); + } + + public void onApplicationDestroy(IApplication application) + { + application.removeApplicationInstanceListener(listener); + } + } + + class ApplicationInstanceNotify implements IApplicationInstanceNotify + { + private final StreamListener listener = new StreamListener(); + + public void onApplicationInstanceCreate(IApplicationInstance appInstance) + { + if (ServerListenerBlocklistStreams.debug) + { + logger.info(MODULE_NAME + ".onApplicationInstanceCreate[" + appInstance.getName() + "] Stream Listener is initiated"); + } + + IMediaStreamNameAliasProvider currentAliasProvider = appInstance.getStreamNameAliasProvider(); + appInstance.setStreamNameAliasProvider(new StreamAliasProvider(currentAliasProvider)); + appInstance.addMediaStreamListener(listener); + } + + public void onApplicationInstanceDestroy(IApplicationInstance appInstance) + { + if (ServerListenerBlocklistStreams.debug) + { + logger.info(MODULE_NAME + ".onApplicationInstanceDestroy[" + appInstance.getName() + "] Stream listener is removed"); + } + appInstance.removeMediaStreamListener(listener); + } + } + + class StreamListener implements IMediaStreamNotify + { + private final IMediaStreamActionNotify actionNotify = new StreamManager(); + + @Override + public void onMediaStreamCreate(IMediaStream stream) + { + + if (ServerListenerBlocklistStreams.debug) + { + logger.info(MODULE_NAME + ".onApplicationInstanceCreate[" + stream.getName() + "] Stream is initiated "); + } + stream.addClientListener(actionNotify); + } + + @Override + public void onMediaStreamDestroy(IMediaStream stream) + { + if (actionNotify != null) + { + stream.removeClientListener(actionNotify); + } + } + } + + class StreamManager implements IMediaStreamActionNotify2 + { + + /* + * (non-Javadoc) + * This will handle the publish event and determine if stream is blocklisted. + */ + @Override + public void onPublish(IMediaStream stream, String streamName, boolean isRecord, boolean isAppend) + { + IApplicationInstance appInstance = stream.getStreams().getAppInstance(); + + // check if the stream being published has been generated locally. They can be controlled elsewhere. + // MediaCaster + if(appInstance.getMediaCasterStreams().getMediaCaster(streamName) != null) + return; + // transcoder output + if(stream.isTranscodeResult()) + return; + // Publisher API + if(stream.isPublisherStream()) + return; + + if (ServerListenerBlocklistStreams.debug) + { + logger.info(MODULE_NAME + ".onApplicationInstanceCreate[" + streamName + "] Checking stream for blocklist"); + } + + String appName = appInstance.getApplication().getName(); + String appInstName = appInstance.getName(); + + if(!BlockListUtils.isStreamblockListed(appName, appInstName, streamName)) + return; + + if (stream.getClient() != null) + { + sendStreamOnStatusError(stream, "NetStream.Publish.BadName", "The publisher's Stream was not white listed"); + stream.getClient().setShutdownClient(true); + + logger.info(MODULE_NAME + ".onPublish[" + streamName + "] Client Rejected (NetStream.Publish.BadName), block listed " + appName + "/" + appInstName + "/" + streamName); + } + else if(stream.getRTPStream() != null) + { + RTPSession session = stream.getRTPStream().getSession(); + if(session != null) + { + appInstance.getVHost().getRTPContext().shutdownRTPSession(session); + logger.info(MODULE_NAME + ".onPublish[" + streamName + "] RTP Rejected, block listed. Stream: " + appName + "/" + appInstName + "/" + streamName); + } + } + } + + @Override + public void onPlay(IMediaStream stream, String streamName, double playStart, double playLen, int playReset) + { + } + + @Override + public void onUnPublish(IMediaStream stream, String streamName, boolean isRecord, boolean isAppend) + { + } + + @Override + public void onPause(IMediaStream stream, boolean isPause, double location) + { + } + + @Override + public void onSeek(IMediaStream stream, double location) + { + } + + @Override + public void onStop(IMediaStream stream) + { + } + + @Override + public void onMetaData(IMediaStream stream, AMFPacket metaDataPacket) + { + } + + @Override + public void onPauseRaw(IMediaStream stream, boolean isPause, double location) + { + } + } + + class StreamAliasProvider implements IMediaStreamNameAliasProvider2 + { + + private final IMediaStreamNameAliasProvider currentAliasProvider; + + public StreamAliasProvider(IMediaStreamNameAliasProvider currentAliasProvider) + { + this.currentAliasProvider = currentAliasProvider; + } + + @Override + public String resolvePlayAlias(IApplicationInstance appInstance, String name) + { + if (BlockListUtils.isStreamblockListed(appInstance.getApplication().getName(), appInstance.getName(), name)) + return null; + return name; + } + + @Override + public String resolvePlayAlias(IApplicationInstance appInstance, String name, IClient client) + { + if(currentAliasProvider != null) + { + if(currentAliasProvider instanceof IMediaStreamNameAliasProvider2) + name = ((IMediaStreamNameAliasProvider2)currentAliasProvider).resolvePlayAlias(appInstance, name, client); + name = currentAliasProvider.resolveStreamAlias(appInstance, name); + } + + if (name == null) + return null; + + if (BlockListUtils.isStreamblockListed(appInstance.getApplication().getName(), appInstance.getName(), name)) + return null; + return name; + } + + @Override + public String resolvePlayAlias(IApplicationInstance appInstance, String name, IHTTPStreamerSession httpSession) + { + if(currentAliasProvider != null) + { + if(currentAliasProvider instanceof IMediaStreamNameAliasProvider2) + name = ((IMediaStreamNameAliasProvider2)currentAliasProvider).resolvePlayAlias(appInstance, name, httpSession); + name = currentAliasProvider.resolveStreamAlias(appInstance, name); + } + + if (name == null) + return null; + + if (BlockListUtils.isStreamblockListed(appInstance.getApplication().getName(), appInstance.getName(), name)) + return null; + return name; + } + + @Override + public String resolvePlayAlias(IApplicationInstance appInstance, String name, RTPSession rtpSession) + { + if(currentAliasProvider != null) + { + if(currentAliasProvider instanceof IMediaStreamNameAliasProvider2) + name = ((IMediaStreamNameAliasProvider2)currentAliasProvider).resolvePlayAlias(appInstance, name, rtpSession); + name = currentAliasProvider.resolveStreamAlias(appInstance, name); + } + + if (name == null) + return null; + + if (BlockListUtils.isStreamblockListed(appInstance.getApplication().getName(), appInstance.getName(), name)) + return null; + return name; + } + + @Override + public String resolvePlayAlias(IApplicationInstance appInstance, String name, ILiveStreamPacketizer liveStreamPacketizer) + { + if(currentAliasProvider != null) + { + if(currentAliasProvider instanceof IMediaStreamNameAliasProvider2) + name = ((IMediaStreamNameAliasProvider2)currentAliasProvider).resolvePlayAlias(appInstance, name, liveStreamPacketizer); + name = currentAliasProvider.resolveStreamAlias(appInstance, name); + } + + if (name == null) + return null; + + if (BlockListUtils.isStreamblockListed(appInstance.getApplication().getName(), appInstance.getName(), name)) + return null; + return name; + } + + @Override + public String resolveStreamAlias(IApplicationInstance appInstance, String name) + { + if(currentAliasProvider != null) + return currentAliasProvider.resolveStreamAlias(appInstance, name); + return name; + } + + @Override + public String resolveStreamAlias(IApplicationInstance appInstance, String name, IMediaCaster mediaCaster) + { + if(currentAliasProvider != null) + { + if(currentAliasProvider instanceof IMediaStreamNameAliasProvider2) + return ((IMediaStreamNameAliasProvider2)currentAliasProvider).resolveStreamAlias(appInstance, name, mediaCaster); + return currentAliasProvider.resolveStreamAlias(appInstance, name); + } + return name; + } + } + + public static final String MODULE_NAME = "ServerListenerblocklistStreams"; + private static final String PROP_NAME_PREFIX = "blocklistStreams"; + + public static boolean debug = false; + + private WMSLogger logger = WMSLoggerFactory.getLogger(getClass()); + private String configPath = null; + + @Override + public void onServerConfigLoaded(IServer server) + { + VHostSingleton.addVHostListener(new VHostNotifier()); + } + + @Override + public void onServerCreate(IServer server) + { + debug = server.getProperties().getPropertyBoolean(PROP_NAME_PREFIX + "DebugLog", debug); + if (logger.isDebugEnabled()) + debug = true; + + this.configPath = server.getProperties().getPropertyStr(ServerListenerBlocklistStreams.PROP_NAME_PREFIX + "ConfigPath", this.configPath); + if (!StringUtils.isEmpty(this.configPath)) + BlockListUtils.setConfigPath(this.configPath); + String separatorChar = server.getProperties().getPropertyStr(ServerListenerBlocklistStreams.PROP_NAME_PREFIX + "SeparatorChar"); + if (!StringUtils.isEmpty(separatorChar)) + BlockListUtils.setSeparatorChar(separatorChar); + } + + @Override + public void onServerInit(IServer server) + { + } + + @Override + public void onServerShutdownComplete(IServer server) + { + } + + @Override + public void onServerShutdownStart(IServer server) + { + } +} From 3af4e1030fdea39adee1cc73f9f64f9b88f2e3d0 Mon Sep 17 00:00:00 2001 From: Amara Date: Fri, 2 Oct 2020 12:25:33 -0500 Subject: [PATCH 3/6] remove old named files, update whitelist --- .../wms/plugin/blacklist/BlackListUtils.java | 212 --------- .../HTTPProviderBlacklistStreams.java | 402 ------------------ .../HTTPProviderBlocklistStreams.java | 2 +- .../ServerListenerBlacklistStreams.java | 382 ----------------- .../ServerListenerBlocklistStreams.java | 2 +- 5 files changed, 2 insertions(+), 998 deletions(-) delete mode 100644 src/com/wowza/wms/plugin/blacklist/BlackListUtils.java delete mode 100644 src/com/wowza/wms/plugin/blacklist/HTTPProviderBlacklistStreams.java delete mode 100644 src/com/wowza/wms/plugin/blacklist/ServerListenerBlacklistStreams.java diff --git a/src/com/wowza/wms/plugin/blacklist/BlackListUtils.java b/src/com/wowza/wms/plugin/blacklist/BlackListUtils.java deleted file mode 100644 index 56bbb8e..0000000 --- a/src/com/wowza/wms/plugin/blacklist/BlackListUtils.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * This code and all components (c) Copyright 2006 - 2018, Wowza Media Systems, LLC. All rights reserved. - * This code is licensed pursuant to the Wowza Public License version 1.0, available at www.wowza.com/legal. - */ -package com.wowza.wms.plugin.blacklist; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileWriter; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.ArrayList; - -import com.wowza.wms.bootstrap.Bootstrap; -import com.wowza.wms.logging.WMSLogger; -import com.wowza.wms.logging.WMSLoggerFactory; - -public class BlackListUtils -{ - private static ArrayList streamBlackList = new ArrayList(); - private static String blacklistConfigPath = Bootstrap.getServerHome(Bootstrap.CONFIGHOME) + "/conf/blacklist.txt"; - static String separatorChar = ":"; - private static WMSLogger logger = WMSLoggerFactory.getLogger(BlackListUtils.class); - - public static void setConfigPath(String config) - { - BlackListUtils.blacklistConfigPath = config; - } - - public static void setSeparatorChar(String separatorChar) - { - BlackListUtils.separatorChar = separatorChar; - } - - public static void blackListStream(String application, String appInstance, String streamName) - { - BlackListUtils.mergeData(); - - synchronized(BlackListUtils.streamBlackList) - { - String streamKey = application + separatorChar + appInstance + separatorChar + streamName; - if (!BlackListUtils.streamBlackList.contains(streamKey)) - { - BlackListUtils.streamBlackList.add(streamKey); - BlackListUtils.printContents(); - BlackListUtils.saveFile(); - } - } - } - - public static void removeStreamFromList(String application, String appInstance, String streamName) - { - BlackListUtils.mergeData(); - String streamKey = application + separatorChar + appInstance + separatorChar + streamName; - if (ServerListenerBlacklistStreams.debug) - logger.info(ServerListenerBlacklistStreams.MODULE_NAME + ".removeStreamFromList[" + streamKey + "] Stream is initiated "); - - synchronized(BlackListUtils.streamBlackList) - { - if (BlackListUtils.streamBlackList.contains(streamKey)) - { - if (BlackListUtils.streamBlackList.remove(streamKey)) - { - BlackListUtils.saveFile(); - BlackListUtils.printContents(); - - logger.info(ServerListenerBlacklistStreams.MODULE_NAME + ".removeStreamFromList[" + streamKey + "] Completed "); - } - else - { - logger.info(ServerListenerBlacklistStreams.MODULE_NAME + ".removeStreamFromList[" + streamKey + "] Does not exist-2 "); - } - } - else - { - logger.info(ServerListenerBlacklistStreams.MODULE_NAME + ".removeStreamFromList[" + streamKey + "] Does not exist "); - } - } - } - - public static boolean isStreamBlackListed(String application, String appInstance, String streamName) - { - BlackListUtils.mergeData(); - String streamKey = application + separatorChar + appInstance + separatorChar + streamName; - return BlackListUtils.streamBlackList.contains(streamKey); - } - - public static ArrayList getBlackListedStreams() - { - BlackListUtils.mergeData(); - synchronized(BlackListUtils.streamBlackList) - { - return BlackListUtils.streamBlackList; - } - } - - public static void mergeData() - { - synchronized(BlackListUtils.streamBlackList) - { - ArrayList fileStored = BlackListUtils.getStoredBlacklistItems(); - if (fileStored.size() > 0) - { - for (int i = 0; i < fileStored.size(); i++) - { - if (!BlackListUtils.streamBlackList.contains(fileStored.get(i))) - { - BlackListUtils.streamBlackList.add(fileStored.get(i)); - } - } - } - } - } - - private static ArrayList getStoredBlacklistItems() - { - ArrayList blackList = new ArrayList(); - File tmpFile = new File(BlackListUtils.blacklistConfigPath); - if (tmpFile.exists()) - { - FileInputStream in = null; - BufferedReader br = null; - try - { - in = new FileInputStream(BlackListUtils.blacklistConfigPath); - br = new BufferedReader(new InputStreamReader(in)); - String cacheItem; - - while ((cacheItem = br.readLine()) != null) - { - cacheItem = cacheItem.trim(); - if (!cacheItem.startsWith("#") && cacheItem.length() > 0) - { - blackList.add(cacheItem); - } - } - } - catch (Exception e) - { - logger.error(ServerListenerBlacklistStreams.MODULE_NAME + ".getStoredBlacklistItems() ", e); - } - finally - { - try - { - if (br != null) - br.close(); - } - catch (IOException e) - { - } - br = null; - try - { - if (in != null) - in.close(); - } - catch (IOException e) - { - } - in = null; - } - } - else - { - logger.info(ServerListenerBlacklistStreams.MODULE_NAME + ".getStoredBlacklistItems could not find black list items list"); - } - return blackList; - } - - private static void saveFile() - { - synchronized(BlackListUtils.streamBlackList) - { - ArrayList bli = BlackListUtils.streamBlackList; - try - { - File f = new File(blacklistConfigPath); - if (!f.exists()) - { - f.createNewFile(); - } - - FileWriter tmpFile = new FileWriter(blacklistConfigPath, false); - StringBuilder b = new StringBuilder(); - for (int i = 0; i < bli.size(); i++) - { - logger.info(ServerListenerBlacklistStreams.MODULE_NAME + ".saveFile " + "Line: " + bli.get(i)); - b.append(bli.get(i) + "\n"); - } - tmpFile.write(b.toString()); - tmpFile.close(); - } - catch (IOException ioe) - { - logger.error(ServerListenerBlacklistStreams.MODULE_NAME + ".saveFile " + "IOException: " + ioe.getMessage(), ioe); - } - } - } - - private static void printContents() - { - ArrayList bli = BlackListUtils.getBlackListedStreams(); - for (int i = 0; i < bli.size(); i++) - { - logger.info(ServerListenerBlacklistStreams.MODULE_NAME + ".printContents " + "bli.get(i): " + bli.get(i)); - } - - } - -} diff --git a/src/com/wowza/wms/plugin/blacklist/HTTPProviderBlacklistStreams.java b/src/com/wowza/wms/plugin/blacklist/HTTPProviderBlacklistStreams.java deleted file mode 100644 index f5a5833..0000000 --- a/src/com/wowza/wms/plugin/blacklist/HTTPProviderBlacklistStreams.java +++ /dev/null @@ -1,402 +0,0 @@ -/* - * This code and all components (c) Copyright 2006 - 2018, Wowza Media Systems, LLC. All rights reserved. - * This code is licensed pursuant to the Wowza Public License version 1.0, available at www.wowza.com/legal. - */ -package com.wowza.wms.plugin.blacklist; - -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import com.wowza.wms.application.IApplication; -import com.wowza.wms.application.IApplicationInstance; -import com.wowza.wms.client.IClient; -import com.wowza.wms.http.HTTProvider2Base; -import com.wowza.wms.http.IHTTPRequest; -import com.wowza.wms.http.IHTTPResponse; -import com.wowza.wms.httpstreamer.model.IHTTPStreamerSession; -import com.wowza.wms.logging.WMSLogger; -import com.wowza.wms.logging.WMSLoggerFactory; -import com.wowza.wms.mediacaster.MediaCasterItem; -import com.wowza.wms.mediacaster.MediaCasterStreamId; -import com.wowza.wms.mediacaster.wowza.LiveMediaStreamURL; -import com.wowza.wms.rtp.model.RTPSession; -import com.wowza.wms.rtp.model.RTPStream; -import com.wowza.wms.stream.IMediaStream; -import com.wowza.wms.vhost.IVHost; - -public class HTTPProviderBlacklistStreams extends HTTProvider2Base -{ - public static final String MODULE_NAME = "HTTPProviderBlacklistStreams"; - - private WMSLogger logger = WMSLoggerFactory.getLogger(getClass()); - - public void onHTTPRequest(IVHost vhost, IHTTPRequest req, IHTTPResponse resp) - { - if (!doHTTPAuthentication(vhost, req, resp)) - return; - - String postResponse = this.handlePost(req, vhost); - - String html = ""; - - // get blacklisted streams - html += this.getBlackListedStreams(); - - // get published streams - html += this.getPublishedStreams(vhost); - - //refresh button - html += this.getRefresh(); - - String postMsg = ""; - if (postResponse != null) - { - postMsg = this.getMsgHeader(postResponse, req); - } - String retStr = this.getHtmlPage(html, postMsg); - try - { - OutputStream out = resp.getOutputStream(); - byte[] outBytes = retStr.getBytes(); - out.write(outBytes); - } - catch (Exception e) - { - logger.error(MODULE_NAME + ".onHTTPRequest()", e); - } - } - - private String getRefresh() - { -// String html = ""; - String html = "

"; - return html; - } - - private String getBlackListedStreams() - { - String html = ""; - ArrayList streams = BlackListUtils.getBlackListedStreams(); - if (streams.size() > 0) - { - - html += this.divHeader("Blacklisted") + this.divSectionStart(); - html += ""; - for (int i = 0; i < streams.size(); i++) - { - String[] parts = streams.get(i).split(BlackListUtils.separatorChar); - if (parts.length == 3) - { - String applicationName = parts[0]; - String appInstanceName = parts[1]; - String streamName = parts[2]; - html += this.addRow(applicationName, appInstanceName, streamName); - } - } - html += "
"; - html += this.divSectionEnd(); - } - - return html; - } - - private String getPublishedStreams(IVHost vhost) - { - String html = ""; - if (vhost != null) - { - html += this.divHeader("Published Streams"); - html += this.divSectionStart(); - html += this.startSection(vhost.getName()); - List appNames = vhost.getApplicationNames(); - if (appNames.size() > 0) - { - Iterator appNameIterator = appNames.iterator(); - while (appNameIterator.hasNext()) - { - String applicationName = appNameIterator.next(); - IApplication application = vhost.getApplication(applicationName); - - List appInstances = application.getAppInstanceNames(); - if (appInstances.size() > 0) - { - Iterator iterAppInstances = appInstances.iterator(); - while (iterAppInstances.hasNext()) - { - String appInstanceName = iterAppInstances.next(); - IApplicationInstance appInstance = application.getAppInstance(appInstanceName); - - Set names = new HashSet(); - List publishedStreams = appInstance.getPublishStreamNames(); - if (publishedStreams.size() > 0) - { - Iterator publishedStreamIterator = publishedStreams.iterator(); - while (publishedStreamIterator.hasNext()) - { - String streamName = publishedStreamIterator.next(); - MediaCasterStreamId mediaCasterStreamId = MediaCasterItem.parseIdString(streamName); - streamName = mediaCasterStreamId.getName(); - if (streamName.indexOf("://") >= 0) - { - String resolvedName = LiveMediaStreamURL.decodeURLToStreamName(streamName); - if (resolvedName != null) - streamName = resolvedName; - } - names.add(streamName); - } - } - for (String name : names) - { - if (!BlackListUtils.isStreamBlackListed(applicationName, appInstanceName, name)) - { - html += this.addRow(applicationName, appInstanceName, name); - } - } - } - } - } - } - else - { - html += this.addEmptyRow("No streams published.."); - } - html += this.endSection(); - html += this.divSectionEnd(); - } - return html; - } - - private boolean blacklistStream(IVHost vhost, String applicationName, String appInstanceName, String streamName) - { - try - { - IApplicationInstance appInstance = vhost.getApplication(applicationName).getAppInstance(appInstanceName); - if (appInstance != null) - { - - // force client disconnect - List rtmpClients = appInstance.getClients(); - List httpClients = appInstance.getHTTPStreamerSessions(); - List rtpClients = appInstance.getRTPSessions(); - - // http - Iterator httpClientIter = httpClients.iterator(); - IHTTPStreamerSession httpClient; - while (httpClientIter.hasNext()) - { - try - { - httpClient = httpClientIter.next(); - if (httpClient == null) - continue; - - if (streamName.equals(httpClient.getStreamName())) - { - httpClient.rejectSession(); - } - } - catch (Exception e) - { - logger.error(MODULE_NAME + ".blacklistStream()", e); - } - - } - - //rtmp - Iterator rtmpClientIter = rtmpClients.iterator(); - IClient rtmpClient; - while (rtmpClientIter.hasNext()) - { - try - { - rtmpClient = rtmpClientIter.next(); - if (rtmpClient == null) - continue; - - List Names = rtmpClient.getPlayStreams(); - - if (Names.size() > 0) - { - Iterator iter = Names.iterator(); - while (iter.hasNext()) - { - IMediaStream stream = (IMediaStream)iter.next(); - - String name = stream.getName(); - if (name.indexOf("://") >= 0) - { - String resolvedName = LiveMediaStreamURL.decodeURLToStreamName(name); - if (resolvedName != null) - name = resolvedName; - } - - if (streamName.equals(name)) - { - rtmpClient.setShutdownClient(true); - } - } - } - } - catch (Exception e) - { - logger.error(MODULE_NAME + ".blacklistStream()", e); - } - } - - //rtp - Iterator rtpClientIter = rtpClients.iterator(); - RTPSession rtpClient; - while (rtpClientIter.hasNext()) - { - try - { - rtpClient = rtpClientIter.next(); - if (rtpClient == null) - continue; - - if (streamName.equals(rtpClient.getRTSPStream().getStreamName())) - { - appInstance.getVHost().getRTPContext().shutdownRTPSession(rtpClient); - } - } - catch (Exception e) - { - logger.error(MODULE_NAME + ".blacklistStream()", e); - } - } - } - - //shutdown stream - IMediaStream publishedStream = appInstance.getStreams().getStream(streamName); - if (publishedStream != null) - { - IClient client = publishedStream.getClient(); - if (client != null) - { - client.setShutdownClient(true); - } - else - { - RTPStream rtpStream = publishedStream.getRTPStream(); - if (rtpStream != null) - { - appInstance.getVHost().getRTPContext().shutdownRTPSession(rtpStream.getSession()); - } - } - } - return true; - } - catch (Exception ex) - { - logger.error(MODULE_NAME + ".blacklistStream()", ex); - } - return false; - } - - private String handlePost(IHTTPRequest req, IVHost vhost) - { - if (req.getMethod().equalsIgnoreCase("post")) - { - req.parseBodyForParams(true); - } - - Map> params = req.getParameterMap(); - System.out.println(params.toString()); - if (params.containsKey("blacklist")) - { - String appPath = params.get("application").get(0) + "/" + params.get("appInstance").get(0) + "/" + params.get("stream").get(0); - if (params.get("blacklist").get(0).equalsIgnoreCase("1")) - { - BlackListUtils.blackListStream(params.get("application").get(0), params.get("appInstance").get(0), params.get("stream").get(0)); - if (this.blacklistStream(vhost, params.get("application").get(0), params.get("appInstance").get(0), params.get("stream").get(0))) - { -// BlackListed.removeStreamFromList(params.get("application").get(0), params.get("appInstance").get(0), params.get("stream").get(0)); - return appPath + " has been added to the blacklist"; - } - return appPath + " failed to add to blacklist."; - } - else - { - BlackListUtils.removeStreamFromList(params.get("application").get(0), params.get("appInstance").get(0), params.get("stream").get(0)); - return appPath + " has been removed from the blacklist"; - } - } - return null; - } - - /* - * HTML Helpers - */ - - private String divSectionStart() - { - return "
"; - } - - private String divSectionEnd() - { - return "
"; - } - - private String divHeader(String title) - { - return "
" + title + "
"; - } - - private String getMsgHeader(String msg, IHTTPRequest req) - { - if (!msg.isEmpty()) - { - String requestURL = req.getRequestURL(); - String[] parts = requestURL.split("\\/"); - String filterName = parts[parts.length - 1]; - msg += " clear"; - } - return "
" + msg + "
"; - } - - private String getHtmlPage(String body, String msg) - { - return "\n" + "\n" + "Blacklist Streams\n" + "\n" + " \n" + " \n

Stream Blacklists

" + "
" + " " + msg + " " + body + " " - + "
" + " \n" + " " - + " " + " " + "
" + "
" + " " + ""; - } - - private String startSection(String vhost) - { - return "
" + vhost + "
" - + " " + " " + " " + " " + ""; - } - - private String endSection() - { - return "
ApplicationAppInstanceStreamAction
"; - } - - private String addEmptyRow(String msg) - { - return "" + msg + ""; - } - - private String addRow(String appName, String appInstance, String streamName) - { - - String blacklist = "1"; - String blacklistedTitle = "Blacklist"; - if (BlackListUtils.isStreamBlackListed(appName, appInstance, streamName)) - { - blacklistedTitle = "Whitelist"; - blacklist = "0"; - } - return "" + " " + appName + "" + " " + appInstance + "" - + " " + streamName + "" + " " - + blacklistedTitle + " " + ""; - } -} diff --git a/src/com/wowza/wms/plugin/blacklist/HTTPProviderBlocklistStreams.java b/src/com/wowza/wms/plugin/blacklist/HTTPProviderBlocklistStreams.java index 948b601..130aa95 100644 --- a/src/com/wowza/wms/plugin/blacklist/HTTPProviderBlocklistStreams.java +++ b/src/com/wowza/wms/plugin/blacklist/HTTPProviderBlocklistStreams.java @@ -392,7 +392,7 @@ private String addRow(String appName, String appInstance, String streamName) String blocklistedTitle = "blocklist"; if (BlockListUtils.isStreamblockListed(appName, appInstance, streamName)) { - blocklistedTitle = "Whitelist"; + blocklistedTitle = "allowlist"; blocklist = "0"; } return "" + " " + appName + "" + " " + appInstance + "" diff --git a/src/com/wowza/wms/plugin/blacklist/ServerListenerBlacklistStreams.java b/src/com/wowza/wms/plugin/blacklist/ServerListenerBlacklistStreams.java deleted file mode 100644 index ba2ccbf..0000000 --- a/src/com/wowza/wms/plugin/blacklist/ServerListenerBlacklistStreams.java +++ /dev/null @@ -1,382 +0,0 @@ -/* - * This code and all components (c) Copyright 2006 - 2018, Wowza Media Systems, LLC. All rights reserved. - * This code is licensed pursuant to the Wowza Public License version 1.0, available at www.wowza.com/legal. - */ -package com.wowza.wms.plugin.blacklist; - -import com.wowza.util.StringUtils; -import com.wowza.wms.amf.AMFDataList; -import com.wowza.wms.amf.AMFPacket; -import com.wowza.wms.application.IApplication; -import com.wowza.wms.application.IApplicationInstance; -import com.wowza.wms.application.IApplicationInstanceNotify; -import com.wowza.wms.application.IApplicationNotify; -import com.wowza.wms.client.IClient; -import com.wowza.wms.httpstreamer.model.IHTTPStreamerSession; -import com.wowza.wms.logging.WMSLogger; -import com.wowza.wms.logging.WMSLoggerFactory; -import com.wowza.wms.mediacaster.IMediaCaster; -import com.wowza.wms.module.ModuleBase; -import com.wowza.wms.module.ModuleCore; -import com.wowza.wms.request.RequestFunction; -import com.wowza.wms.rtp.model.RTPRequestStatus; -import com.wowza.wms.rtp.model.RTPSession; -import com.wowza.wms.rtp.model.RTPStream; -import com.wowza.wms.server.IServer; -import com.wowza.wms.server.IServerNotify2; -import com.wowza.wms.stream.IMediaStream; -import com.wowza.wms.stream.IMediaStreamActionNotify; -import com.wowza.wms.stream.IMediaStreamActionNotify2; -import com.wowza.wms.stream.IMediaStreamNameAliasProvider; -import com.wowza.wms.stream.IMediaStreamNameAliasProvider2; -import com.wowza.wms.stream.IMediaStreamNotify; -import com.wowza.wms.stream.livepacketizer.ILiveStreamPacketizer; -import com.wowza.wms.util.MediaCasterUtils; -import com.wowza.wms.vhost.IVHost; -import com.wowza.wms.vhost.IVHostNotify; -import com.wowza.wms.vhost.VHostSingleton; - -public class ServerListenerBlacklistStreams extends ModuleBase implements IServerNotify2 -{ - private class VHostNotifier implements IVHostNotify - { - private final ApplicationNotify listener = new ApplicationNotify(); - - @Override - public void onVHostCreate(IVHost vhost) - { - vhost.addApplicationListener(listener); - } - - @Override - public void onVHostInit(IVHost vhost) - { - } - - @Override - public void onVHostShutdownStart(IVHost vhost) - { - } - - @Override - public void onVHostShutdownComplete(IVHost vhost) - { - vhost.removeApplicationListener(listener); - } - - @Override - public void onVHostClientConnect(IVHost vhost, IClient inClient, RequestFunction function, AMFDataList params) - { - } - - } - - class ApplicationNotify implements IApplicationNotify - { - private final ApplicationInstanceNotify listener = new ApplicationInstanceNotify(); - - public void onApplicationCreate(IApplication application) - { - application.addApplicationInstanceListener(listener); - } - - public void onApplicationDestroy(IApplication application) - { - application.removeApplicationInstanceListener(listener); - } - } - - class ApplicationInstanceNotify implements IApplicationInstanceNotify - { - private final StreamListener listener = new StreamListener(); - - public void onApplicationInstanceCreate(IApplicationInstance appInstance) - { - if (ServerListenerBlacklistStreams.debug) - { - logger.info(MODULE_NAME + ".onApplicationInstanceCreate[" + appInstance.getName() + "] Stream Listener is initiated"); - } - - IMediaStreamNameAliasProvider currentAliasProvider = appInstance.getStreamNameAliasProvider(); - appInstance.setStreamNameAliasProvider(new StreamAliasProvider(currentAliasProvider)); - appInstance.addMediaStreamListener(listener); - } - - public void onApplicationInstanceDestroy(IApplicationInstance appInstance) - { - if (ServerListenerBlacklistStreams.debug) - { - logger.info(MODULE_NAME + ".onApplicationInstanceDestroy[" + appInstance.getName() + "] Stream listener is removed"); - } - appInstance.removeMediaStreamListener(listener); - } - } - - class StreamListener implements IMediaStreamNotify - { - private final IMediaStreamActionNotify actionNotify = new StreamManager(); - - @Override - public void onMediaStreamCreate(IMediaStream stream) - { - - if (ServerListenerBlacklistStreams.debug) - { - logger.info(MODULE_NAME + ".onApplicationInstanceCreate[" + stream.getName() + "] Stream is initiated "); - } - stream.addClientListener(actionNotify); - } - - @Override - public void onMediaStreamDestroy(IMediaStream stream) - { - if (actionNotify != null) - { - stream.removeClientListener(actionNotify); - } - } - } - - class StreamManager implements IMediaStreamActionNotify2 - { - - /* - * (non-Javadoc) - * This will handle the publish event and determine if stream is blacklisted. - */ - @Override - public void onPublish(IMediaStream stream, String streamName, boolean isRecord, boolean isAppend) - { - IApplicationInstance appInstance = stream.getStreams().getAppInstance(); - - // check if the stream being published has been generated locally. They can be controlled elsewhere. - // MediaCaster - if(appInstance.getMediaCasterStreams().getMediaCaster(streamName) != null) - return; - // transcoder output - if(stream.isTranscodeResult()) - return; - // Publisher API - if(stream.isPublisherStream()) - return; - - if (ServerListenerBlacklistStreams.debug) - { - logger.info(MODULE_NAME + ".onApplicationInstanceCreate[" + streamName + "] Checking stream for blacklist"); - } - - String appName = appInstance.getApplication().getName(); - String appInstName = appInstance.getName(); - - if(!BlackListUtils.isStreamBlackListed(appName, appInstName, streamName)) - return; - - if (stream.getClient() != null) - { - sendStreamOnStatusError(stream, "NetStream.Publish.BadName", "The publisher's Stream was not white listed"); - stream.getClient().setShutdownClient(true); - - logger.info(MODULE_NAME + ".onPublish[" + streamName + "] Client Rejected (NetStream.Publish.BadName), black listed " + appName + "/" + appInstName + "/" + streamName); - } - else if(stream.getRTPStream() != null) - { - RTPSession session = stream.getRTPStream().getSession(); - if(session != null) - { - appInstance.getVHost().getRTPContext().shutdownRTPSession(session); - logger.info(MODULE_NAME + ".onPublish[" + streamName + "] RTP Rejected, black listed. Stream: " + appName + "/" + appInstName + "/" + streamName); - } - } - } - - @Override - public void onPlay(IMediaStream stream, String streamName, double playStart, double playLen, int playReset) - { - } - - @Override - public void onUnPublish(IMediaStream stream, String streamName, boolean isRecord, boolean isAppend) - { - } - - @Override - public void onPause(IMediaStream stream, boolean isPause, double location) - { - } - - @Override - public void onSeek(IMediaStream stream, double location) - { - } - - @Override - public void onStop(IMediaStream stream) - { - } - - @Override - public void onMetaData(IMediaStream stream, AMFPacket metaDataPacket) - { - } - - @Override - public void onPauseRaw(IMediaStream stream, boolean isPause, double location) - { - } - } - - class StreamAliasProvider implements IMediaStreamNameAliasProvider2 - { - - private final IMediaStreamNameAliasProvider currentAliasProvider; - - public StreamAliasProvider(IMediaStreamNameAliasProvider currentAliasProvider) - { - this.currentAliasProvider = currentAliasProvider; - } - - @Override - public String resolvePlayAlias(IApplicationInstance appInstance, String name) - { - if (BlackListUtils.isStreamBlackListed(appInstance.getApplication().getName(), appInstance.getName(), name)) - return null; - return name; - } - - @Override - public String resolvePlayAlias(IApplicationInstance appInstance, String name, IClient client) - { - if(currentAliasProvider != null) - { - if(currentAliasProvider instanceof IMediaStreamNameAliasProvider2) - name = ((IMediaStreamNameAliasProvider2)currentAliasProvider).resolvePlayAlias(appInstance, name, client); - name = currentAliasProvider.resolveStreamAlias(appInstance, name); - } - - if (name == null) - return null; - - if (BlackListUtils.isStreamBlackListed(appInstance.getApplication().getName(), appInstance.getName(), name)) - return null; - return name; - } - - @Override - public String resolvePlayAlias(IApplicationInstance appInstance, String name, IHTTPStreamerSession httpSession) - { - if(currentAliasProvider != null) - { - if(currentAliasProvider instanceof IMediaStreamNameAliasProvider2) - name = ((IMediaStreamNameAliasProvider2)currentAliasProvider).resolvePlayAlias(appInstance, name, httpSession); - name = currentAliasProvider.resolveStreamAlias(appInstance, name); - } - - if (name == null) - return null; - - if (BlackListUtils.isStreamBlackListed(appInstance.getApplication().getName(), appInstance.getName(), name)) - return null; - return name; - } - - @Override - public String resolvePlayAlias(IApplicationInstance appInstance, String name, RTPSession rtpSession) - { - if(currentAliasProvider != null) - { - if(currentAliasProvider instanceof IMediaStreamNameAliasProvider2) - name = ((IMediaStreamNameAliasProvider2)currentAliasProvider).resolvePlayAlias(appInstance, name, rtpSession); - name = currentAliasProvider.resolveStreamAlias(appInstance, name); - } - - if (name == null) - return null; - - if (BlackListUtils.isStreamBlackListed(appInstance.getApplication().getName(), appInstance.getName(), name)) - return null; - return name; - } - - @Override - public String resolvePlayAlias(IApplicationInstance appInstance, String name, ILiveStreamPacketizer liveStreamPacketizer) - { - if(currentAliasProvider != null) - { - if(currentAliasProvider instanceof IMediaStreamNameAliasProvider2) - name = ((IMediaStreamNameAliasProvider2)currentAliasProvider).resolvePlayAlias(appInstance, name, liveStreamPacketizer); - name = currentAliasProvider.resolveStreamAlias(appInstance, name); - } - - if (name == null) - return null; - - if (BlackListUtils.isStreamBlackListed(appInstance.getApplication().getName(), appInstance.getName(), name)) - return null; - return name; - } - - @Override - public String resolveStreamAlias(IApplicationInstance appInstance, String name) - { - if(currentAliasProvider != null) - return currentAliasProvider.resolveStreamAlias(appInstance, name); - return name; - } - - @Override - public String resolveStreamAlias(IApplicationInstance appInstance, String name, IMediaCaster mediaCaster) - { - if(currentAliasProvider != null) - { - if(currentAliasProvider instanceof IMediaStreamNameAliasProvider2) - return ((IMediaStreamNameAliasProvider2)currentAliasProvider).resolveStreamAlias(appInstance, name, mediaCaster); - return currentAliasProvider.resolveStreamAlias(appInstance, name); - } - return name; - } - } - - public static final String MODULE_NAME = "ServerListenerBlacklistStreams"; - private static final String PROP_NAME_PREFIX = "blacklistStreams"; - - public static boolean debug = false; - - private WMSLogger logger = WMSLoggerFactory.getLogger(getClass()); - private String configPath = null; - - @Override - public void onServerConfigLoaded(IServer server) - { - VHostSingleton.addVHostListener(new VHostNotifier()); - } - - @Override - public void onServerCreate(IServer server) - { - debug = server.getProperties().getPropertyBoolean(PROP_NAME_PREFIX + "DebugLog", debug); - if (logger.isDebugEnabled()) - debug = true; - - this.configPath = server.getProperties().getPropertyStr(ServerListenerBlacklistStreams.PROP_NAME_PREFIX + "ConfigPath", this.configPath); - if (!StringUtils.isEmpty(this.configPath)) - BlackListUtils.setConfigPath(this.configPath); - String separatorChar = server.getProperties().getPropertyStr(ServerListenerBlacklistStreams.PROP_NAME_PREFIX + "SeparatorChar"); - if (!StringUtils.isEmpty(separatorChar)) - BlackListUtils.setSeparatorChar(separatorChar); - } - - @Override - public void onServerInit(IServer server) - { - } - - @Override - public void onServerShutdownComplete(IServer server) - { - } - - @Override - public void onServerShutdownStart(IServer server) - { - } -} diff --git a/src/com/wowza/wms/plugin/blacklist/ServerListenerBlocklistStreams.java b/src/com/wowza/wms/plugin/blacklist/ServerListenerBlocklistStreams.java index ed101d2..39f5e45 100644 --- a/src/com/wowza/wms/plugin/blacklist/ServerListenerBlocklistStreams.java +++ b/src/com/wowza/wms/plugin/blacklist/ServerListenerBlocklistStreams.java @@ -173,7 +173,7 @@ public void onPublish(IMediaStream stream, String streamName, boolean isRecord, if (stream.getClient() != null) { - sendStreamOnStatusError(stream, "NetStream.Publish.BadName", "The publisher's Stream was not white listed"); + sendStreamOnStatusError(stream, "NetStream.Publish.BadName", "The publisher's Stream was not allow listed"); stream.getClient().setShutdownClient(true); logger.info(MODULE_NAME + ".onPublish[" + streamName + "] Client Rejected (NetStream.Publish.BadName), block listed " + appName + "/" + appInstName + "/" + streamName); From 5a3cd7d81e5238b353aab8c0171c5bbf0eee4d32 Mon Sep 17 00:00:00 2001 From: Amara Date: Fri, 2 Oct 2020 12:35:54 -0500 Subject: [PATCH 4/6] renaming dir --- src/com/wowza/wms/plugin/blocklist/.DS_Store | Bin 0 -> 6148 bytes .../{blacklist => blocklist}/BlockListUtils.java | 0 .../HTTPProviderBlocklistStreams.java | 0 .../ServerListenerBlocklistStreams.java | 0 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/com/wowza/wms/plugin/blocklist/.DS_Store rename src/com/wowza/wms/plugin/{blacklist => blocklist}/BlockListUtils.java (100%) rename src/com/wowza/wms/plugin/{blacklist => blocklist}/HTTPProviderBlocklistStreams.java (100%) rename src/com/wowza/wms/plugin/{blacklist => blocklist}/ServerListenerBlocklistStreams.java (100%) diff --git a/src/com/wowza/wms/plugin/blocklist/.DS_Store b/src/com/wowza/wms/plugin/blocklist/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..40f3d307c18febf212d6e75f008a113fe8a190ee GIT binary patch literal 6148 zcmeHKO-sW-5Pe&FsCX%O@wlSBD*gcpp@M=3A$srUL5&7#%{l+bpQvwU$6ASc5k$&N zn0dqO&J20GWU~N5{yaMY`T%-V!Ok|#7b4@LE$LWfix{-VJuY#JCoFN(n(cxA$bhWf zE^?HpFva@%ExD2<*YXH=7^BAcqO7KqvYIeMw#zB?_|=5|KF)E?=ZepQ)oSL?FsJ`f zy|mp?aEB~7Hpasb9bQ;u%7GvEwtGy^=d zMS434-8uu#fHSaSK)w%=Dwr8I3H8%KqgMc8mtht9@^dIT(J(V?5^{tRQYtZ}2D@T} zl+Jjxd6{98Fr_2diW}GOtOvN};1iG3Uxryhl}GJc>ii44Z_EQ2a+gX>j8V H{3!!(t?*8P literal 0 HcmV?d00001 diff --git a/src/com/wowza/wms/plugin/blacklist/BlockListUtils.java b/src/com/wowza/wms/plugin/blocklist/BlockListUtils.java similarity index 100% rename from src/com/wowza/wms/plugin/blacklist/BlockListUtils.java rename to src/com/wowza/wms/plugin/blocklist/BlockListUtils.java diff --git a/src/com/wowza/wms/plugin/blacklist/HTTPProviderBlocklistStreams.java b/src/com/wowza/wms/plugin/blocklist/HTTPProviderBlocklistStreams.java similarity index 100% rename from src/com/wowza/wms/plugin/blacklist/HTTPProviderBlocklistStreams.java rename to src/com/wowza/wms/plugin/blocklist/HTTPProviderBlocklistStreams.java diff --git a/src/com/wowza/wms/plugin/blacklist/ServerListenerBlocklistStreams.java b/src/com/wowza/wms/plugin/blocklist/ServerListenerBlocklistStreams.java similarity index 100% rename from src/com/wowza/wms/plugin/blacklist/ServerListenerBlocklistStreams.java rename to src/com/wowza/wms/plugin/blocklist/ServerListenerBlocklistStreams.java From 803b6d8f9f5f6ec2e3c41eade0d9f7037ce44de8 Mon Sep 17 00:00:00 2001 From: "Amara Graham (Keller)" Date: Fri, 2 Oct 2020 12:39:07 -0500 Subject: [PATCH 5/6] Remaining possible updates to now --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dcc846e..a85ca06 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Wowza Streaming Engine 4.0.0 or later is required. To blocklist a stream: -1. Start your Wowza Streaming Engine media server, and then open the following URL to the Stream Blocklists page in a web browser:
http://[wowza-ip-address]:8086/blacklist
+1. Start your Wowza Streaming Engine media server, and then open the following URL to the Stream Blocklists page in a web browser:
http://[wowza-ip-address]:8086/blocklist
2. When prompted, enter the credentials that you use to sign in to Wowza Streaming Engine Manager. From 87665a224a90487536f1add4467818d1c9fa0e76 Mon Sep 17 00:00:00 2001 From: Amara Date: Fri, 2 Oct 2020 12:45:30 -0500 Subject: [PATCH 6/6] updated jar --- lib/wse-plugin-blackliststreams.jar | Bin 18148 -> 19888 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/lib/wse-plugin-blackliststreams.jar b/lib/wse-plugin-blackliststreams.jar index 596b84ee2fa824ecfb4ec883681a33d9ce3927b4..481e634a6c01231d8872c36e90a9a8af6fdd0004 100644 GIT binary patch literal 19888 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 J%H#>+0RXZ8UU>ij literal 18148 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