Wowza Community

Custom stream authorization and expiration module (all methods?)

I wanted my stream links to expire after a certain time period, so I devised a system where I would generate a unique hash signature based on the stream path, the current timestamp, and a secret code. Then, I would include this signature and timestamp as variables with my embedded streams, and the Wowza server could use my secret code to re-create the hash using the stream path and timestamp variable. If the re-created hash doesn’t match the included signature, the stream request is rejected. It also rejects requests where the timestamp is too old (expired).

In addition to the Wowza IDE, I used PHP and JW Player 5.2.

I started by using PHP to generate the new variables:

$secret_code = 'mysecret';
$streampath = 'path/to/stream.mp4';
$timestamp = time();
$signature = md5($streampath . $timestamp . $secret_code);

Then I added these new variables (timestamp and signature) as URL variables to my embedded RTSP and HTTP streams (the variable names don’t matter here, but the order does):

echo $streampath . '?t=' . $timestamp . '&s=' . $signature;

For RTMP, I edited the JW Player source code to support receiving and sending two new flash variables (timestamp and signature). In PlayerConfig.as I added:

		/** Extra variables for authorizing streams via flashvars **/
		protected var _timestamp:String 		= null;
		protected var _signature:String 		= null;

		/** Extra functions for authorizing streams via flashvars **/
		public function get timestamp():String { return _timestamp; }
		public function set timestamp(x:String):void { _timestamp = x; }
		public function get signature():String { return _signature; }
		public function set signature(x:String):void { _signature = x; }

And in RTMPMediaProvider.as, I replaced the line:

_connection.connect(item.streamer);

with the line:

_connection.connect(item.streamer,item.file,config.timestamp,config.signature);

Then I compiled and ran the new JW Player, adding the following flashvars to my javascript/embed code:

  so.addVariable('timestamp','<?php echo $timestamp; ?>');
  so.addVariable('signature','<?php echo $signature; ?>');
  
  &timestamp=<?php echo $timestamp; ?>&signature=<?php echo $signature; ?>

Then, the only thing left was to tell the server what to do with the new variables it receives, and accept/reject the stream request based on them. So in the Wowza IDE, I compiled the following module (my Java skills are poor, so you could definitely clean this up a bit if you wanted):

package com.unite.wms.module;

import com.wowza.wms.application.*;
import com.wowza.wms.amf.*;
import com.wowza.wms.client.*;
import com.wowza.wms.module.*;
import com.wowza.wms.request.*;
import com.wowza.wms.stream.*;
import com.wowza.wms.rtp.model.*;
import com.wowza.wms.httpstreamer.model.*;
import com.wowza.wms.httpstreamer.cupertinostreaming.httpstreamer.*;
import com.wowza.wms.httpstreamer.smoothstreaming.httpstreamer.*;
import java.security.*;
import java.math.*;
import java.lang.Long;
import java.lang.String;

public class Authorization extends ModuleBase {
	
	// Define the same secret code here that you used in the PHP page
	public String secret_code = "mysecret";
	
	// Define how long you want the stream to be valid, in seconds
	public long seconds = 60;
	
	// For RTMP stream requests
	public void onConnect(IClient client, RequestFunction function, AMFDataList params)
	{
		// default to not authorized
		Boolean authorized = false;
		
		try {
			// get flashvar variables
			String filename = getParamString(params, PARAM1);
			String timestamp = getParamString(params, PARAM2);
			String signature = getParamString(params, PARAM3);

			// check authorization
			authorized = authorize(filename,timestamp,signature);
		} catch (NullPointerException e) {
			getLogger().info("RTMP Authorization failed!");
		} catch (ArrayIndexOutOfBoundsException e) {
			getLogger().info("RTMP Authorization failed!");
		}

		// log status and reject connection, if necessary
		if (!authorized)
		{
			getLogger().info("RTMP Authorization rejected");
			sendClientOnStatusError(client, "NetConnection.Connect.Rejected", "Access denied: please reload the video page.");
			client.rejectConnection();
		} else
		{
			getLogger().info("RTMP Authorization approved");
		}
	}

	// For HTTP Cupertino stream requests
	public void onHTTPCupertinoStreamingSessionCreate(HTTPStreamerSessionCupertino session)
	{
		// default to not authorized		
		Boolean authorized = false;
		
		try {
			// get URL variables
			String filename = session.getStreamName();
			String querystring = session.getStream().getQueryStr();
			String[] queryarray = querystring.split("[&]");
			String[] temparray = queryarray[0].split("[=]");
			String timestamp = temparray[1];
			temparray = queryarray[1].split("[=]");
			String signature = temparray[1];

			// check authorization
			authorized = authorize(filename,timestamp,signature);
		} catch (NullPointerException e) {
			getLogger().info("HTTP Cupertino Authorization failed!");
		} catch (ArrayIndexOutOfBoundsException e) {
			getLogger().info("HTTP Cupertino Authorization failed!");
		}

		// log status and reject connection, if necessary
		if (!authorized)
		{
			getLogger().info("HTTP Cupertino Authorization rejected");
			session.rejectSession();
		} else
		{
			getLogger().info("HTTP Cupertino Authorization approved");
		}
	}

	// for HTTP Smooth Streaming stream requests
	public void onHTTPSmoothStreamingSessionCreate(HTTPStreamerSessionSmoothStreamer session)
	{
		// default to not authorized		
		Boolean authorized = false;
		
		try {
			// get URL variables
			String filename = session.getStreamName();
			String querystring = session.getStream().getQueryStr();
			String[] queryarray = querystring.split("[&]");
			String[] temparray = queryarray[0].split("[=]");
			String timestamp = temparray[1];
			temparray = queryarray[1].split("[=]");
			String signature = temparray[1];

			// check authorization
			authorized = authorize(filename,timestamp,signature);
		} catch (NullPointerException e) {
			getLogger().info("HTTP Smooth Streaming Authorization failed!");
		} catch (ArrayIndexOutOfBoundsException e) {
			getLogger().info("HTTP Smooth Streaming Authorization failed!");
		}

		// log status and reject connection, if necessary
		if (!authorized)
		{
			getLogger().info("HTTP Smooth Streaming Authorization rejected");
			session.rejectSession();
		} else
		{
			getLogger().info("HTTP Smooth Streaming approved");
		}
	}

	// For RTP/RTSP stream requests
	public void onRTPSessionCreate(RTPSession rtspSession)
	{
		// default to not authorized
		Boolean authorized = false;
		
		try {
			// get full URL
			String uri = rtspSession.getUri();
			
			// split URL to get stream path
			Boolean b = uri.matches("(?i).*mp4:.*");
			String[] temparray;
			if ( !b ) {
				temparray = uri.split("_definst_/");
			} else {
				temparray = uri.split("mp4:");
			}
			String filename = temparray[1];
			
			// get URL variables
			String querystring = rtspSession.getQueryStr();
			String[] queryarray = querystring.split("[&]");
			temparray = queryarray[0].split("[=]");
			String timestamp = temparray[1];
			temparray = queryarray[1].split("[=]");
			String signature = temparray[1];

			// check authorization
			authorized = authorize(filename,timestamp,signature);
		} catch (NullPointerException e) {
			getLogger().info("RTP Authorization failed!");
		} catch (ArrayIndexOutOfBoundsException e) {
			getLogger().info("RTP Authorization failed!");
		}

		// log status and reject connection, if necessary
		if (!authorized)
		{
			getLogger().info("RTP Authorization rejected");
			rtspSession.rejectSession();
		} else
		{
			getLogger().info("RTP Authorization approved");
		}
	}

	// main authorize function
	public boolean authorize(String filename, String timestamp, String signature)
	{
		// default to not authorized
		Boolean authorized = false;
		
		try {
			// First compare the timestamp to the current time, and reject if beyond specified number of seconds
			long currenttime = System.currentTimeMillis()/1000;
			long futuretime = Long.parseLong(timestamp.trim()) + seconds;
			Integer comparetime = new Long(currenttime).compareTo(new Long(futuretime));
			if ( comparetime <= 0 ) {

				// Now proceed checking that the signature matches the given filename and timestamp	
				String hashcheck = filename + timestamp + secret_code;
				MessageDigest m;
				m = MessageDigest.getInstance("MD5");
				m.reset();
				m.update(hashcheck.getBytes());
				byte[] digest = m.digest();
				BigInteger bigInt = new BigInteger(1,digest);
				String hashtext = bigInt.toString(16);
				while(hashtext.length() < 32 ){
					hashtext = "0"+hashtext;
				}

				if ( signature.equals(hashtext) ) {
					authorized = true;
				}
			}
		} catch (NoSuchAlgorithmException e) {
			getLogger().info("Authorization algorithm failed!");
		}

		return authorized;
	}

}

Finally, I compiled this new module, made sure the resulting “authorization.jar” file was in my Wowza “lib” folder, then included it into my Wowza application configs (conf/appname/application.xml):

            <Module>
				<Name>Authorization</Name>
				<Description>Stream authorization and expiration</Description>
				<Class>com.unite.wms.module.Authorization</Class>
            </Module>

Restart the server, and bingo! It should only accept connections that include a valid signature, and only for the time period specified in your Java code.

It’s not working for me and was wondering if you had any suggestions I followed the directions…and did it exactly as you have it laid out and it doesn’t refuse any connections at all.

Do you have any ideas on where I might have gone wrong? I’m not using jw player and using straight links rtsp://… and iphone http://…/playlist.m3u8 links instead…could this be causing any issues?

The steps I’m taking to compile it in the Wowza IDE are as following:

I have the authorization.java file open I click export->java (folder) jar file->next->the current project folder->next->…seal the jar

Geoff,

When you copy this code into the Wowza IDE, then save it, if there are no errors, the jar file is compiled here with no other actions:

[wowza-install-dir]/lib/[project-name].jar

This is the file you move to the remote server’s Wowza /lib folder. Then restart that Wowza server.

Richard

Can you zip up the /conf and /logs folders and send them to support@wowza.com. You can try to attach to email, but sip attachments bounce sometimes so it is preferable to post the zip files to a web server and send a link to download.

Please include a link to this thread.

Richard

You can write to support@wowza.com and ask for list of independent consultants.

Richard

Graeme,

Thanks for the write-up. I will forward internally.

Richard

I just compiled it again from the posted source and it worked for me.

Do you get any messages in your Wowza access log from this module? If it’s compiled and installed correctly, you should get “Authorization approved” or “Authorization rejected” messages in the Wowza log for every stream request in that application. If you see these messages, but it’s still not behaving properly, you can add more logging to the code for debugging purposes:

getLogger().info(“Sample module logpoint”);

You can even log any of the Java variables if you want to check their values:

getLogger().info("Query string: " + querystring);

If you still don’t see any messages in the log, then it’s probably not compiled or installed correctly. Check the logs for error messages, and double-check your Application.xml file. Feel free to post some of your code here (your PHP page, Authorization.java, and Application.xml) and I can look it over too.

Thanks Geoff – not sure how I missed that, but I will update my code. Glad to hear you got it working.

capostrike93,

I am currently using my code, and it is working fine (although I’m sure it could be cleaner and more efficient). Your code seems to add more specific logging, which may be desired, but I don’t think it improves the functionality.

benoit934,

Do you get that “NumberFormatException” error even when passing the correct hash? That could indicate there is an error in how you are creating or processing the hash.

Otherwise, if the script is still accepting the correct hash without any errors, you could just add extra catch statements to deal with that error you are receiving on a bad hash (much like geoff_b found above with the “ArrayIndexOutOfBoundsException”):

catch (NumberFormatException e) {

getLogger().info(“HTTP Cupertino Authorization failed!”);

}

I Can’t livestream with Adobe Flash Media Live Encoder

Help Please :frowning:

What do you mean you can’t stream with FMLE?

Take a look at this guide to get started:

How to set up live streaming using an RTMP-based encoder

Salvadore

Help Please Thx

Yeah I it doesn’t appear to be compiled correctly I am using EXACTLY the same code you provided the only two things I changed were the package path and secret word.

I also changed the path in Application.xml to match that package path.

Could it be the steps I’m using in the wowza ide to export it? Here is how I’m currently doing it:

This is the authorization.java file before exporting:

package com.mycompany.wms.module;
import com.wowza.wms.application.*;
import com.wowza.wms.amf.*;
import com.wowza.wms.client.*;
import com.wowza.wms.module.*;
import com.wowza.wms.request.*;
import com.wowza.wms.stream.*;
import com.wowza.wms.rtp.model.*;
import com.wowza.wms.httpstreamer.model.*;
import com.wowza.wms.httpstreamer.cupertinostreaming.httpstreamer.*;
import com.wowza.wms.httpstreamer.smoothstreaming.httpstreamer.*;
import java.security.*;
import java.math.*;
import java.lang.Long;
import java.lang.String;
public class Authorization extends ModuleBase {
	// Define the same secret code here that you used in the PHP page
	public String secret_code = "mycompanysecretvod";
	// Define how long you want the stream to be valid, in seconds
	public long seconds = 60;
	// For RTMP stream requests
	public void onConnect(IClient client, RequestFunction function, AMFDataList params)
	{
		// default to not authorized
		Boolean authorized = false;
		try {
			// get flashvar variables
			String filename = getParamString(params, PARAM1);
			String timestamp = getParamString(params, PARAM2);
			String signature = getParamString(params, PARAM3);
			// check authorization
			authorized = authorize(filename,timestamp,signature);
		} catch (NullPointerException e) {
			getLogger().info("RTMP Authorization failed!");
		}
		// log status and reject connection, if necessary
		if (!authorized)
		{
			getLogger().info("RTMP Authorization rejected");
			sendClientOnStatusError(client, "NetConnection.Connect.Rejected", "Access denied: please reload the video page.");
			client.rejectConnection();
		} else
		{
			getLogger().info("RTMP Authorization approved");
		}
	}
	// For HTTP Cupertino stream requests
	public void onHTTPCupertinoStreamingSessionCreate(HTTPStreamerSessionCupertino session)
	{
		// default to not authorized
		Boolean authorized = false;
		try {
			// get URL variables
			String filename = session.getStreamName();
			String querystring = session.getStream().getQueryStr();
			String[] queryarray = querystring.split("[&]");
			String[] temparray = queryarray[0].split("[=]");
			String timestamp = temparray[1];
			temparray = queryarray[1].split("[=]");
			String signature = temparray[1];
			// check authorization
			authorized = authorize(filename,timestamp,signature);
		} catch (NullPointerException e) {
			getLogger().info("HTTP Cupertino Authorization failed!");
		}
		// log status and reject connection, if necessary
		if (!authorized)
		{
			getLogger().info("HTTP Cupertino Authorization rejected");
			session.rejectSession();
		} else
		{
			getLogger().info("HTTP Cupertino Authorization approved");
		}
	}
	// for HTTP Smooth Streaming stream requests
	public void onHTTPSmoothStreamingSessionCreate(HTTPStreamerSessionSmoothStreamer session)
	{
		// default to not authorized
		Boolean authorized = false;
		try {
			// get URL variables
			String filename = session.getStreamName();
			String querystring = session.getStream().getQueryStr();
			String[] queryarray = querystring.split("[&]");
			String[] temparray = queryarray[0].split("[=]");
			String timestamp = temparray[1];
			temparray = queryarray[1].split("[=]");
			String signature = temparray[1];
			// check authorization
			authorized = authorize(filename,timestamp,signature);
		} catch (NullPointerException e) {
			getLogger().info("HTTP Smooth Streaming Authorization failed!");
		}
		// log status and reject connection, if necessary
		if (!authorized)
		{
			getLogger().info("HTTP Smooth Streaming Authorization rejected");
			session.rejectSession();
		} else
		{
			getLogger().info("HTTP Smooth Streaming approved");
		}
	}
	// For RTP/RTSP stream requests
	public void onRTPSessionCreate(RTPSession rtspSession)
	{
		// default to not authorized
		Boolean authorized = false;
		try {
			// get full URL
			String uri = rtspSession.getUri();
			// split URL to get stream path
			Boolean b = uri.matches("(?i).*mp4:.*");
			String[] temparray;
			if ( !b ) {
				temparray = uri.split("_definst_/");
			} else {
				temparray = uri.split("mp4:");
			}
			String filename = temparray[1];
			// get URL variables
			String querystring = rtspSession.getQueryStr();
			String[] queryarray = querystring.split("[&]");
			temparray = queryarray[0].split("[=]");
			String timestamp = temparray[1];
			temparray = queryarray[1].split("[=]");
			String signature = temparray[1];
			// check authorization
			authorized = authorize(filename,timestamp,signature);
		} catch (NullPointerException e) {
			getLogger().info("RTP Authorization failed!");
		}
		// log status and reject connection, if necessary
		if (!authorized)
		{
			getLogger().info("RTP Authorization rejected");
			rtspSession.rejectSession();
		} else
		{
			getLogger().info("RTP Authorization approved");
		}
	}
	// main authorize function
	public boolean authorize(String filename, String timestamp, String signature)
	{
		// default to not authorized
		Boolean authorized = false;
		try {
			// First compare the timestamp to the current time, and reject if beyond specified number of seconds
			long currenttime = System.currentTimeMillis()/1000;
			long futuretime = Long.parseLong(timestamp.trim()) + seconds;
			Integer comparetime = new Long(currenttime).compareTo(new Long(futuretime));
			if ( comparetime <= 0 ) {
				// Now proceed checking that the signature matches the given filename and timestamp
				String hashcheck = filename + timestamp + secret_code;
				MessageDigest m;
				m = MessageDigest.getInstance("MD5");
				m.reset();
				m.update(hashcheck.getBytes());
				byte[] digest = m.digest();
				BigInteger bigInt = new BigInteger(1,digest);
				String hashtext = bigInt.toString(16);
				while(hashtext.length() < 32 ){
					hashtext = "0"+hashtext;
				}
				if ( signature.equals(hashtext) ) {
					authorized = true;
				}
			}
		} catch (NoSuchAlgorithmException e) {
			getLogger().info("Authorization algorithm failed!");
		}
		return authorized;
	}
}

and here is the Application.xml file:

                                <Name>Authorization</Name>
                                <Description>Stream authorization and expiration</Description>
                                <Class>com.mycompany.wms.module.Authorization</Class>

The steps I’m taking to compile it in the Wowza IDE are as following:

I have the authorization.java file open I click export->java (folder) jar file->next->the current project folder->next->…seal the jar

and upload it to the server in the lib folder.

Oh and the PHP code is as follows

<?php for ($i=0; $i<count($VIDEO_FILENAMES); ++$i): ?>
<?php
		$streampath= $VIDEO_FILENAMES[$i];
		$secret_code = 'mycompanysecretvod';
		$timestamp = time();
		$signature = md5($streampath . $timestamp . $secret_code);
		$uri_prefix = "rtsp://ipaddress:1935/vod/";
		$http_moviepath = $uri_prefix .$streampath."?t=".$timestamp."&s=".$signature;
?>
<a href="<?=$http_moviepath;?>">Video <?=$i+1;?></a>

Turns out my service provider had my permissions set incorrectly and I wasn’t actually restarting the service…which was the reason for this confusion, right now it’s refusing all connection attempts though I will be posting questions as I figure this out.

Here is where things stand. I doesn’t seem to accept any connections. However if I take the querystring off it does say that connection has failed as opposed to rejected like it does on all other attempts. So it does at least seem to be checking the querystring as opposed to rejecting everything out of hand.

good code…

but I would use …

[PHP]

public void play(IClient client, RequestFunction function, AMFDataList params)

{

try

{

String[] data = null;

data = getParamString(params, PARAM1).toLowerCase().split("&");

String filename = data[0];

String timestamp = data[1];

String signature = data[2];

try

{

long currenttime = System.currentTimeMillis()/1000;

long futuretime = Long.parseLong(timestamp.trim()) + seconds;

Integer comparetime = new Long(currenttime).compareTo(new Long(futuretime));

if ( comparetime <= 0 )

{

String hashcheck = filename + timestamp + secret_code;

MessageDigest m;

m = MessageDigest.getInstance(“MD5”);

m.reset();

m.update(hashcheck.getBytes());

byte[] digest = m.digest();

BigInteger bigInt = new BigInteger(1,digest);

String hashtext = bigInt.toString(16);

while(hashtext.length() < 32 )

{

hashtext = “0”+hashtext;

}

if ( !signature.equals(hashtext) )

{

sendClientOnStatusError(client, “NetConnection.Connect.Rejected”, “Access denied: bad token.”);

client.rejectConnection();

getLogger().info(“RTMP Authorization rejected, bad token”);

return;

}

}

else

{

sendClientOnStatusError(client, “NetConnection.Connect.Rejected”, “Access denied: time bad.”);

client.rejectConnection();

getLogger().info(“RTMP Authorization rejected, time bad”);

return;

}

} catch (NoSuchAlgorithmException e) {

getLogger().info(“Authorization algorithm failed!”);

sendClientOnStatusError(client, “NetConnection.Connect.Rejected”, “Access denied: server failed.”);

return;

}

} catch (NullPointerException e) {

getLogger().info(“RTMP Authorization failed!”);

sendClientOnStatusError(client, “NetConnection.Connect.Rejected”, “Access denied: server failed.”);

return;

}

this.invokePrevious(client, function, params);

}

[/PHP]

and

[PHP]

//…

nc.connect(“rtmp://localhost/live”);

//…

nsPlay.play(“thestream&thetimestamp&thesignature”);

[/PHP]

sorry for my bad english

Thank you for the response however I’m not even concerned with rtmp streaming just cupertino for iphone and rtsp for android not even using flash. Thanks again for the time I really really appreciate it.

I am going to take your ideas though and use them for what I’m doing that’s really good for logging and getting more accurate ideas what’s wrong. Thank you.

Got it compiled and working thank you for everyone’s help…I just had to make some changes to match up what the module was looking for vs. my php. This would have been fixed much sooner had my service provider actually been allowing me to restart the server.

I spoke to soon. When trying to connect to iphone via cupertino it’s works unless there is no value then it automatically approves the request. So it appears this section is not working

catch (NullPointerException e) {

getLogger().info(“HTTP Cupertino Authorization failed!”);

}

It doesn’t appear to ever catch this. However if I put a value in incorrectly it does reject it such as the secret word. I’m a little stumped.

The logs are showing that there is an error: java.lang.ArrayIndexOutOfBoundsException: 1: com.host.wms.module.Authorization.onHTTPCupertinoStreamingSessionCreate(Authorization.java:69) but not failing it.