Skip to content

Commit

Permalink
Merge pull request #39 from sul-dlss/rtmp
Browse files Browse the repository at this point in the history
adding stacks token authorization to "play" method for rtmp
  • Loading branch information
jmartin-sul authored Aug 15, 2016
2 parents 2ddba0c + 2d5306c commit c300655
Show file tree
Hide file tree
Showing 8 changed files with 825 additions and 455 deletions.
2 changes: 1 addition & 1 deletion conf/example/Application.xml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
</Properties>
</TimedText>
<!-- HTTPStreamers (separate with commas): cupertinostreaming, smoothstreaming, sanjosestreaming, mpegdashstreaming, dvrchunkstreaming -->
<HTTPStreamers>cupertinostreaming, sanjosestreaming, mpegdashstreaming</HTTPStreamers>
<HTTPStreamers>cupertinostreaming, mpegdashstreaming</HTTPStreamers>
<MediaCache>
<MediaCacheSourceList></MediaCacheSourceList>
</MediaCache>
Expand Down
2 changes: 1 addition & 1 deletion conf/version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.4.4-beta
0.4.4
Binary file added lib/wms-mediacache.jar
Binary file not shown.
84 changes: 68 additions & 16 deletions src/edu/stanford/dlss/wowza/SulWowza.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,21 @@
import org.apache.commons.validator.routines.InetAddressValidator;
import com.google.common.escape.Escaper;
import com.google.common.net.PercentEscaper;
import com.wowza.wms.amf.AMFDataList;
import com.wowza.wms.application.ApplicationInstance;
import com.wowza.wms.application.IApplicationInstance;
import com.wowza.wms.client.IClient;
import com.wowza.wms.httpstreamer.cupertinostreaming.httpstreamer.HTTPStreamerSessionCupertino;
import com.wowza.wms.httpstreamer.model.IHTTPStreamerSession;
import com.wowza.wms.httpstreamer.mpegdashstreaming.httpstreamer.HTTPStreamerSessionMPEGDash;
import com.wowza.wms.httpstreamer.sanjosestreaming.httpstreamer.HTTPStreamerSessionSanJose;
import com.wowza.wms.module.ModuleBase;
import com.wowza.wms.request.RequestFunction;

/** Stanford University Libraries Wowza Plugin Code */
public class SulWowza extends ModuleBase
{

static String stacksTokenVerificationBaseUrl;
static String stacksUrlErrorMsg = "rejecting due to invalid stacksURL property (" + stacksTokenVerificationBaseUrl + ")";
static int stacksConnectionTimeout;
static int stacksReadTimeout;

Expand All @@ -40,7 +43,7 @@ public void onAppStart(IApplicationInstance appInstance)
try
{
new URL(stacksTokenVerificationBaseUrl);
getLogger().info(this.getClass().getSimpleName() + "stacksURL is " + stacksTokenVerificationBaseUrl);
getLogger().info(this.getClass().getSimpleName() + " stacksURL is " + stacksTokenVerificationBaseUrl);
}
catch (MalformedURLException e)
{
Expand All @@ -56,7 +59,7 @@ public void onHTTPMPEGDashStreamingSessionCreate(HTTPStreamerSessionMPEGDash htt
{
if (invalidConfiguration)
{
getLogger().error(this.getClass().getSimpleName() + " onHTTPMPEGDashStreamingSessionCreate: rejecting session due to invalid stacksURL property " + httpSession.getStreamName());
getLogger().error(this.getClass().getSimpleName() + " onHTTPMPEGDashStreamingSessionCreate: " + stacksUrlErrorMsg + "; streamName: " + httpSession.getStreamName());
httpSession.rejectSession();
}
else
Expand All @@ -73,7 +76,7 @@ public void onHTTPCupertinoStreamingSessionCreate(HTTPStreamerSessionCupertino h
{
if (invalidConfiguration)
{
getLogger().error(this.getClass().getSimpleName() + " onHTTPCupertinoStreamingSessionCreate: rejecting session due to invalid stacksURL property " + httpSession.getStreamName());
getLogger().error(this.getClass().getSimpleName() + " onHTTPCupertinoStreamingSessionCreate: " + stacksUrlErrorMsg + "; streamName: " + httpSession.getStreamName());
httpSession.rejectSession();
}
else
Expand All @@ -83,22 +86,41 @@ public void onHTTPCupertinoStreamingSessionCreate(HTTPStreamerSessionCupertino h
}
}

/** Invoked when an Adobe HDS streaming session is created (known as San Jose Streaming in Wowza parlance).
* This is an HTTP-based (port 80) streaming protocol for Flash. We currently use this for streaming MP3
* files, since there are no current HLS players that support MP3 across all browsers. */
public void onHTTPSanJoseStreamingSessionCreate(HTTPStreamerSessionSanJose httpSession)
{
/**
* Invoked when a media file starts playing. Defined in ModuleCore. This seems to be the only place
* we can reliably intercept a Flash connection and get the name of the stream.
*/
public void play(IClient client, RequestFunction function, AMFDataList params)
{
String streamName = params.getString(PARAM1);

//get the real stream name if this is an alias.
streamName = ((ApplicationInstance)client.getAppInstance()).internalResolvePlayAlias(streamName, client);

if (invalidConfiguration)
{
getLogger().error(this.getClass().getSimpleName() + " onHTTPSanJoseinoStreamingSessionCreate: rejecting session due to invalid stacksURL property " + httpSession.getStreamName());
httpSession.rejectSession();
getLogger().error(this.getClass().getSimpleName() + " play: " + stacksUrlErrorMsg + "; streamName: " + streamName);
client.shutdownClient();
}
else
{
getLogger().info(this.getClass().getSimpleName() + " onHTTPSanJoseStreamingSessionCreate: " + httpSession.getStreamName());
authorizeSession(httpSession);
String queryString = client.getQueryStr();
// we've empirically observed that the query string is coming over in the streamName, at least some of the time
if (queryString == null || queryString.length() == 0)
queryString = streamName;
String clientIP = client.getIp();

if (authorizePlay(queryString, clientIP, streamName))
this.invokePrevious(client, function, params);
else
{
getLogger().error(this.getClass().getSimpleName() + " failed to authorize streamName " + streamName + ", queryStr " + queryString);
sendClientOnStatusError(client, "NetStream.Play.Failed", "Rejected due to invalid token");
client.shutdownClient();
}
}
}
}


// --------------------------------- the public API is above this line ----------------------------------------

Expand Down Expand Up @@ -208,6 +230,25 @@ void authorizeSession(IHTTPStreamerSession httpSession)
httpSession.rejectSession();
}

boolean authorizePlay(String queryStr, String userIp, String streamName)
{
String stacksToken = getStacksToken(queryStr);
if (validateStacksToken(stacksToken) && validateUserIp(userIp) && validateStreamName(streamName))
{
String druid = getDruid(streamName);
String filename = getFilename(streamName);

getLogger().debug(this.getClass().getSimpleName() + " userIp: " + userIp);
getLogger().debug(this.getClass().getSimpleName() + " streamName: " + streamName);
if (druid != null && filename != null && verifyStacksToken(stacksToken, druid, filename, userIp))
return true;
else
return false;
}
else
return false;
}

/** Assumption: stacksToken, druid, userIp and filename are all reasonable values (non-null, not empty, etc.) */
boolean verifyStacksToken(String stacksToken, String druid, String filename, String userIp)
{
Expand Down Expand Up @@ -317,7 +358,18 @@ String getDruid(String streamName)
* Assumption: validateStreamName() was already called */
String getFilename(String streamName)
{
String filename = streamName.substring(streamName.lastIndexOf('/') + 1);
String myStreamName = streamName;
// remove query string suffix if present
int questionMarkIndex = streamName.indexOf('?');
if (questionMarkIndex > 0)
myStreamName = streamName.substring(0, questionMarkIndex);

String filename = myStreamName.substring(myStreamName.lastIndexOf('/') + 1);
// remove protocol prefix if present
int colonIndex = filename.indexOf(':');
if (colonIndex > 0)
filename = filename.substring(colonIndex + 1);

if (filename.length() > 0)
return filename;
else
Expand Down
114 changes: 114 additions & 0 deletions test/edu/stanford/dlss/wowza/TestParsingFromRequestInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package edu.stanford.dlss.wowza;

import static org.junit.Assert.*;

import org.junit.*;


/** test parsing of data from streamName and/or queryString */
public class TestParsingFromRequestInfo
{
SulWowza testModule;
final static String stacksToken = "encryptedStacksMediaToken";
final static String queryStr = "stacks_token=" + stacksToken;
final static String streamName = "aa/000/aa/0000/mp4:example.mp4";

@Before
public void setUp()
{
testModule = new SulWowza();
}

@Test
public void getStacksToken_queryStrOnly()
{
assertEquals(stacksToken, testModule.getStacksToken(queryStr));
}

@Test
public void getStacksToken_streamNameWithQueryStr()
{
assertEquals(stacksToken, testModule.getStacksToken(streamName + "?" + queryStr));
}

@Test
public void getStacksToken_ignoresOtherQueryParams()
{
String myQueryStr = "ignored=ignored&stacks_token=" + stacksToken + "&anything=anythingElse";
assertEquals(stacksToken, testModule.getStacksToken(myQueryStr));
assertEquals(stacksToken, testModule.getStacksToken(streamName + "?" + myQueryStr));
}

@Test
public void getStacksTokenEmptyWhenEmptyTokenParam()
{
String myQueryStr = "ignored=ignored&stacks_token=&anything=anythingElse";
assertEquals("", testModule.getStacksToken(myQueryStr));
assertEquals("", testModule.getStacksToken(streamName + "?" + myQueryStr));
}

@Test
public void getStacksTokenNullWhenMissingTokenParam()
{
String myQueryStr = "ignored=ignored&anything=anythingElse";
assertNull(testModule.getStacksToken(myQueryStr));
assertNull(testModule.getStacksToken(streamName + "?" + myQueryStr));
}

@Test
public void getStacksTokenNullWhenEmptyStr()
{
assertNull(testModule.getStacksToken(""));
}

@Test
public void getStacksTokenNullWhenNullArg()
{
SulWowza testModule = new SulWowza();
assertNull(testModule.getStacksToken(null));
}

@Test
public void getDruid()
{
assertEquals("oo000oo0000", testModule.getDruid("oo/000/oo/0000/stream.mp4")); // oo000oo000 has valid druid format
assertEquals("oo000oo0000", testModule.getDruid("oo/000/oo/0000/a"));
assertNull(testModule.getDruid("oo/00/oo/0000/")); // oo00oo0000 is not a valid druid format
assertNull(testModule.getDruid("a/b/c/d/anything.without%slash")); // abcd is not a valid druid format
assertNull(testModule.getDruid("a/b/c/d/"));
}

@Test
public void getDruidWhenQueryParams()
{
assertEquals("oo000oo0000", testModule.getDruid("oo/000/oo/0000/stream.mp4" + "?" + queryStr));
assertEquals("oo000oo0000", testModule.getDruid("oo/000/oo/0000/a" + "?" + queryStr));
}

@Test
public void getFilename()
{
assertEquals("stream.mp4", testModule.getFilename("oo/00/oo/0000/stream.mp4"));
assertEquals("anything.without%slash", testModule.getFilename("a/b/c/d/anything.without%slash"));
assertNull(testModule.getFilename("a/b/c/d/"));
}

@Test
public void getFilenameWhenProtocol()
{
assertEquals("stream.mp4", testModule.getFilename("oo/00/oo/0000/mp4:stream.mp4"));
assertEquals("anything.without%slash", testModule.getFilename("a/b/c/d/mp3:anything.without%slash"));
assertNull(testModule.getFilename("a/b/c/d/mp3:"));
}

@Test
public void getFilenameWhenQueryParams()
{
assertEquals("stream.mp4", testModule.getFilename("oo/00/oo/0000/stream.mp4?" + queryStr));
assertEquals("stream.mp4", testModule.getFilename("oo/00/oo/0000/mp4:stream.mp4?" + queryStr));
assertEquals("anything.without%slash", testModule.getFilename("a/b/c/d/anything.without%slash?" + queryStr));
assertEquals("anything.without%slash", testModule.getFilename("a/b/c/d/mp3:anything.without%slash?" + queryStr));
assertNull(testModule.getFilename("a/b/c/d/?" + queryStr));
assertNull(testModule.getFilename("a/b/c/d/mp3:?" + queryStr));
}
}
Loading

0 comments on commit c300655

Please sign in to comment.