Protect your assets with expiring URLs

Note: This article has been marked for a quality review and will soon be updated.

A problem that many web developers face is how to protect their assets. Specifically ones that users are supposed to pay for - say a video training site for instance, you don't want every Tom, Dick and Harry being able to guess the URL, or worse crack open the source code to find an unprotected URL. IF this happens companies can lose a lot of money, so it's kind of an important subject. The more astute of you out there (or those that read the title), might have already guessed that today we will be building a system that will allow us to force expiring URLs. Head over to the example to see what we'll be creating here.

To do this we won't be naming our files anything too funky, nor will we leave polite messages in our source; instead we will create a media-serving script that will make sure the request isn't coming from an unscrupulous source. For that we will need to get time on our side, and generate unique hash-keys that will allow us to verify the request. On a side note you could have a system which connected to a database to see if a hash existed, and if it wasn't expired, but for this tutorial, we will stick with using good ol' time. We'll only request our protected files from our script, and send 2 parameters to make sure we get what we want, they will be our file path, and our token in hash form. We will also use some nifty htaccess trickery to ensure users don't just copy and past the file path from our URL to their browser window - because that would just invalidate what we are about to do. Let's get started!

To begin we will create our media serving script, so go ahead and create a new file called asset.php - this script will decide if the request is genuine and if so will serve up our media. Here is the code for that file:

$t = $_GET['t'];
$url = $_GET['url'];

if(!empty($t) && !empty($url)){
	if($t==md5(date('his').'my_amazing_salt')){
		$image = imagecreatefromjpeg($url);
		header('Content-Type: image/jpeg');
		imagejpeg($image);	
	}else {
		echo 'Invalid URL!';
	}
}else {
	echo 'Invalid URL!';
}

Now the code above works under the pretence that we are only serving jpegs - if you wanted to only serve something like pngs for instance, you would just need to alter the content type, and fiddle with the loading of the actual file. So what does it actually do? Well at the top we make sure the variables passed to the script in the URL aren't empty, and then go on to validate them. The token system we're using here is one that generates a hash-key based on the current time, along with our own 'my_amazing_salt' string (just to make things even more secure) - which is then encrypted using the handy md5() function. We then load in the image, set the header (so it displays as an image and not a bunch of random characters), and serve up our media. If any of the checks fail along the way we just output a little message to our users.

If you want to have links that expire after an minute for example, you would simply alter the parameter passed to the date() function to something like "hi" for hour and minutes - check out the date function for more info.

Great, we've done the hard part, now we just need to replace any image URLs on our site with ones that point to our script that we just made. We can generate the URLs like so:

$url = 'asset.php?url=food.jpg&t='.md5(date('his').'my_amazing_salt'); 

And just alter the 'url' parameter where appropriate. I would recommend picking a totally random salt to add to the end of your encryption just so others won't have a clue what it is.

Ok, so now we have a working system whereby we can request our media from one file, and if that URL is visited by the user after the page load, they will be shown the door. But we still have one problem. Users will still be able to access our files using the path we pass to our script - they could just copy and paste it into their browser, and hey presto they've gotten what they wanted. So how do we fix this? Well the best way is using some htaccess magic to prevent direct loading of our media files.

Whack the following line of code into your htaccess file (if it doesn't exist, create one in your media directory):

RewriteEngine On
RewriteRule ^(.*).jpg$ asset.php?src=$1 [L]

That rule above simply sends all requests for images with the extension 'jpg' to the script we just created; and as we know, because the redirect will go to a URL without a token, it will invariably be rejected. At this point you might want to alter the code above to reflect whatever kind of media you are trying to protect.

And that's how to protect your assets! This method is useful when you want to conserve bandwidth and stop users from simply grabbing a load of subscription/payment-based files from your server.

Update

Is would seem the kids these days are into also protecting these moving-picture thingy-ma-jigs, so I thought I'd update this post to encompass protecting videos. Depending on what video format you wish to use, some of the code below will need changing, but I'll highlight that when we get there.

The code is nearly identical to what we used for images, with a couple of alterations, so here's the code you put in your .htaccess file:

RewriteEngine On

RewriteRule ^(.*).m4v$ asset.php?src=$1 [L]

Notice the ".m4v" extension - this is something you'll have to alter if you're using another format.

Then our PHP file changes to:

$t = $_GET['t'];
$url = $_GET['url'];

if(!empty($t) && !empty($url)){
	if($t==md5(date('his').'my_amazing_salt')){
		$filename = $_GET['url'];
		$fsize = filesize($filename);
		header('Content-Disposition: filename='.$url);
		header('Content-Type: video/quicktime');
		header('Content-Length: '.(string)$fsize);
		readfile($filename);
	}else {
		echo 'Invalid URL';
	}
}else {
	echo 'Invalid URL';
}
	

Again, notice the file extension here and alter accordingly. To include the video on your page you can do all manner of things, for instance you might want to use a flash player or the swanky <video> tag. In the video example I've used an iFrame.

And that's how to protect videos!

Adding Vignettes to Images with CSS

Vignettes are something we've been able to add to images in Photoshop for many years - it describes the technique of reducing clarity towards the edges of images by reducing things like saturation and brightness, and can often give an image a different quality. It was for that reason that I decided I wanted to use them in my designs, but I certainly didn't want to use images that were pre-composed with the effect, and so decided it would be simple to add them using CSS. What I didn't know, was that there were many things conspiring to prevent me achieving the effect properly. So without further adieu, allow me to share my experience with you.

I first read Trent Walton's article on vignetting and implemented that on the site, but soon realised its shortcomings. Firstly the use of negative z-indexing played havoc with my backgrounds - this is because its fine if you have an all-white background, but if you have an image, it simply throws the picture behind it. And secondly I found myself frustrated with the fact that you couldn't right click and save the image with ease. I also took a look at Chris Coyier's article on the subject, but once again hit a brick wall.

So after a lot of playing around and many alterations I came up with a solution that is the combination of many techniques that I have come across, and the one use on the site today.

My HTML is as follows.

<figure>	
    <img src="path/to/image" alt="Test image" />
</figure>

The class of left simply floats the element to the left, while I use the following CSS to style the rest.

figure {
	position: relative;
	display: block; 
}

	figure.vignette img {
		z-index: 1;
		position: relative;
		display: block;
	}

	figure::before {
		content: "";
		position: absolute;
		top: -1em;
		bottom: -1em;
		left: -1em;
		right: -1em;
	}

	figure.vignette::after {
		position: absolute;
		top: 0;
		bottom: 0;
		left: 0;
		right: 0;
		content: "";
		z-index: 2;
		pointer-events: none;
		width:100%;
		height:100%;
		box-shadow:inset 0px 0px 85px rgba(0,0,0,.5);
		-webkit-box-shadow:inset 0px 0px 85px rgba(0,0,0,.5);
		-moz-box-shadow:inset 0px 0px 85px rgba(0,0,0,.5);
		border-radius: 3px;
		-moz-border-radius: 3px;
		-webkit-border-radius: 3px;
	}

So the main styling of the figure element simply makes sure it is visible and sets it's position to relative, as does the styling of the image within it; but it is the code that follows that does all the magic. We use the pseudo ::before and ::after to apply the vignette effect. If you are unfamiliar with those selectors, visit W3's article on the subject. You can increase the intensity of the vignette by altering the box-shadow property to have a less transparent shadow, or bigger radius. You could even have different colours by altering the same property.

And that's it! I haven't found any inherent problems with this technique just yet, and although this method isn't really apply a vignette in the traditional sense, I feel it does a pretty good imitation of it.

Resizing images – the right way

Something that is very important on the web today is how fast our websites load, and alongside script optimization and other fancy tricks, images are a large part of our sites, and so it's important that they are optimized for the web. However what if we don't have control over the size of the images, and we don't have a program to re-size them? Well we have to do something else. I am talking in terms of a CMS, but the techniques I am about to show you can be used anywhere to create many different sized images from one image.

The first method works but is a little clumsy, take a look.

img { max-width:800px; }

The code above would be placed in your CMS file and stops all images from getting bigger than a margin of 800px. Yes it works, but there are a number of problems with it. If I had a huge image, say 5000 x 5000px, then this method of resizing would mean my image would take a long time to load, this is because the browser still goes to get the huge image before resizing it - this is bad! I strongly recommend you don't use this method, but instead explore other options.

Well first off for images that are integral to your site, you should be able to re-size them using your original design, but for images that have yet to be uploaded and put on display on your site, I suggest the use of JavaScript. When I first began learning how to re-size images with JavaScript I believed that it was the only way, you see to do so you have to use a large amount of code and it requires you to have a good knowledge of JavaScript. However a very kind man who goes by the name of Tim McDaniels wrote a rather lovely script that we can use for resizing images. The script is called TimThumb and is really easy to use.

Head on over to Google Code and download it (or you could just use the URL provided by Google). Once you have uploaded it onto your own server you can start using it straight away. I find it really useful for use in blogs and as I mentioned above, in a CMS. So when you are ready you can implement it's functionality by replacing the URL of your image with the URL of the script and a bit of other info - see below.

<img src="timthumb.php?src=my-image.jpg&width=100&height=100&zc=1" alt="">

The above code tells the script where your image is, the dimensions you would like it re-sized to, and that you would like crop the image while zooming - that's the little 'zc' parameter by the way. The way in which the script operates is this, it takes the image and all the information with it, and then creates a cache folder to store the image. It then creates the image and returns the URL of the resized image to be used in the 'src' attribute. The script offers some customisation, for example you can change the directory the script saves the images to, so that they can be used again. Just see the documentation for details.

And that's it really! I just wanted to share that lovely script with you and ward you off resizing using CSS. Users will appreciate the shorted loading time of pages and I can assure you Google will too!