Posted on

Why did you do this, Adam?

With the death of goo.gl, I decided to develop my own personal short-URL service (which is live at r.blakey.family). You can access the code by viewing the project on GitHub.

I’ve also now decided to make the service available for anyone that would like to on another domain — r.dabll.com. This is the DABLL Redirectererer and I’ll talk a little about how it works below.

How does it work?

Database connection

The program is written in PHP and uses a MYSQL database to store the shortlinks. The code below sets up a connection to the database, gives an error if the connection failed, then also looks up to see if the ‘redirect’ table already exists by looking up all rows with an ‘ID’ column (which is hopefully all of them!).

If the table doesn’t exist, then we create the table with the structure given on line 23 and insert an example record below (so the empty table test works above in future). The important parts of the structure are obviously the `ID`, `URL`, and `shortname` columns.

// *******************
// DATABASE CONNECTION
// *******************
// Makes connection with the database containing the redirect data.
$connection = new mysqli("{YOUR_HOST_NAME}", "{YOUR_USERNAME}", "{YOUR_USER_PASSWORD}", "{YOUR_DATABASE_NAME}");
// Dies with an error message if the connection to the database fails.
if($connection->connect_error)
{
	die("Connection to database failed: ".$connection->connect_error);
}
// Creates the 'redirect' table if it doesn't already exist, and inserts a new record for Google (just so that the table has at least one record).
$result = $connection->query("SELECT ID FROM redirect");
if(empty($result))
{
	$connection->query("CREATE TABLE redirect(ID varchar(20) NOT NULL, URL varchar(255) NOT NULL, shortname varchar(255), PRIMARY KEY(ID), timestamp int(11) NOT NULL, IP varchar(45) NOT NULL, proxyIP varchar(45) NOT NULL)");
	$connection->query("INSERT INTO redirect (ID, URL, shortname, timestamp, IP, proxyIP) VALUES ('".substr(md5('http://google.com'), 0, 6)."', 'http://google.com', 'google', ".time().", '".$_SERVER['REMOTE_ADDR']."', '".$_SERVER['HTTP_X_FORWARDED_FOR']."')");
}

Redirection

The program works by taking URLs of the form ‘{YOUR_DOMAIN}?r=abcxyz’, so we now check to see if the ‘r’ GET variable is set. If it is set, then we lookup to the database to find where the shortname provided in ‘r’ links to.

We also add the protocol (HTTP or HTTPS) in case it is missing in the corresponding URL.

We then send a header out to the client to tell them to go elsewhere!

// ***********
// REDIRECTION
// ***********
if(isset($_GET['r']))
{
	// Gets the URL associated with either the shortname or ID.
	$url = $connection->query("SELECT URL FROM redirect WHERE `shortname`='".$_GET['r']."' OR `ID`='".$_GET['r']."' LIMIT 1");

	// Gets the URL to redirect to.
	$redirectURL = $url->fetch_object()->URL;

	// Adds the protocol if it is missing, defaulting to HTTP.
	if ((strpos($redirectURL, 'http://') === false) && (strpos($redirectURL, 'https://') === false))
	{
	    $redirectURL = 'http://'.$redirectURL;
	}

	// Redirects to the URL.
	header("Location: ".$redirectURL); // Script will end here if r is set.
}

Adding new URLs

There is the ability within this program to also add new shortlinks. We do this by checking to see if the ‘a’ GET variable has been set. If it has, then we display a form asking the user to input a new URL to be shortened. As a bonus, we can take the value passed into ‘a’ and use this to prefill the URL value on the form.

We make the shortname optional here and will use a random shortname if none is given.

If ‘a’ isn’t set and the form has been submitted, then we lookup to see if the URL given is already in the database (because in this program we don’t allow multiple shortnames for the same URL) and we tell the user this and give them the option to use that shortname themselves.

Obviously in a public environment the behaviour may not be as desired (you may want to password-protect this, for example).

// ********
// ADDITION
// ********
if(isset($_GET['a'])) // Displays stuff for adding.
{
	?>
	<form method="POST" action="/">
		<label for="URL">URL</label>
			<input type="text" name="URL" value="<?php echo $_GET['a']; ?>">
		<label for="shortname">Shortname (optional)</label>
			<input type="text" name="shortname">
		<input type="submit" name="submit">
	</form>
	<?php
	// YOU SHOULD PROBABLY ASK FOR THE MAGIC WORDS (you should probably password-protect the adding of new shortlinks).
}
elseif(isset($_POST['submit'])) // Displays stuff for submitting new addition.
{
	// Makes a query to see if the URL already exists in the database.
	$urlQuery = $connection->query("SELECT ID, shortname FROM redirect WHERE `URL`='".$_POST['URL']."' LIMIT 1");

	// If a result is returned, then display an error telling the user this.
	if ($urlQuery->num_rows == 1)
	{
		// Stores the result from the earlier query.
		$result = $urlQuery->fetch_object();

		// Sets a base from where new shortlinks should be calculated.
		$baseURL = "{YOUR_BASE_URL}"; // For example, use something like "goo.gl" here.

		// Gives the redirection URL for the ID found.
		$urlID = $baseURL."?r=".$result->ID;

		// Displays a message to the user teling them that the ID exists with the following redirection URL.
		echo "The URL is already in the database at <a href='https://".$urlID."'>".$urlID."</a>.";

		// If the shortname exists, then we display the shortname redirection URL as well.
		if ($result->shortname != NULL)
		{
			$urlShortname = $baseURL."r=".$result->shortname;
			echo " Or try <a href='https://".$urlShortname."'>".$urlShortname."</a>.";
		}

After those checks, we’re ready to try and insert the shortname and URL into the database. We get a sufficiently random ID for our record by taking the first 6 characters of the MD5-hash of the URL. In case of a collision we try 99 more times with the adjacent ID (after all if you’re still going after that then you are the definition of unlucky).

We exit the loop once we don’t find a conflicting record in the database. We then insert the record and tell the user what their shortlinks are. There’ll potentially be two that work — the ID of the record, and also the shortname (if one was provided).

// This section is run if the URL isn't already in the database.
// We will now check to see if the ID exists.

// Variable for controlling the loop.
$trying = true;

// This is our trial ID, which is the MD5 hash of the URL (to make it sufficiently random).
$ID = substr(md5($_POST['URL']), 0, 6);
$newID = $ID; // This is what our ID will ultimately be.

// We loop while we have a conflict, and while we haven't tried more than 100 times.
while ($trying and abs($newID - $ID) < 100)
{
	$incrementQuery = $connection->query("SELECT ID FROM redirect WHERE `ID`='".$newID."' LIMIT 1");

	if ($incrementQuery->num_rows == 1)
	{
		$newID++;
	}
	else
	{
		$trying = false;
	}
}

if (abs($newID - $ID) != 100)
{
	$connection->query("INSERT INTO redirect (ID, URL, shortname, timestamp, IP, proxyIP) VALUES ('".$newID."', '".$_POST['URL']."', '".$_POST['shortname']."', ".time().", '".$_SERVER['REMOTE_ADDR']."', '".$_SERVER['HTTP_X_FORWARDED_FOR']."')");

	if (true) // error
	{
		$getURL = $urlQuery = $connection->query("SELECT ID, shortname FROM redirect WHERE `URL`='".$_POST['URL']."' LIMIT 1");
		$result = $getURL->fetch_object();

		if ($result->shortname != NULL)
		{
			?>
			<p>Your shortlinks are <a href="<?php echo $baseURL."?r=".$result->shortname; ?>">r.blakey.family?r=<?php echo $result->shortname; ?></a>, or <a href=<?php echo $baseURL."?r=".$result->ID; ?>">r.blakey.family?r=<?php echo $result->ID; ?></a>.</p>
			<?php
		}
		else
		{
			?>
			<p> Your shortlink is <a href="<?php echo $baseURL."?r=".$result->shortname; ?>">r.blakey.family?r=<?php echo $result->shortname; ?></a>.</p>
			<?php
		}
	}
	else
	{
		// Some error message
	}				
}

Go use it

This needs polishing to be used in a real situation (there are definitely more error checks that need to be made), but you can get the idea about how a program like this might work.

It might be nice, for example, to remove the need for users to add the ‘?r=’ to every request. Adding something like the following to your .htaccess file may solve this:

RewriteCond %{REQUEST_URI} !^/index.php
RewriteCond %{REQUEST_FILENAME}    !-f
RewriteCond %{REQUEST_FILENAME}    !-d
RewriteRule ^(.*)$ /index.php?r=$1 [L, QSA]

If you have a suggestion on how to improve this, want to throw abuse at me, or want to tell me about your neighbour’s cat, then send me a message on social media using the links in the footer of the website or the contact page.

You can find a working version of this at https://r.dabll.com.

Leave a Reply

Your email address will not be published. Required fields are marked *