Månedlige arkiver: desember 2010

Hotlinkere er som oftest fjortiser

Jeg skrev tidligere en liten innføring i hvordan jeg har stoppet hotlinkere fra å vise bilder liggende på bloggen min. Der beskrev jeg hvordan jeg brukte .htaccess filen for å hindre de jeg la til i listen. Dette fungerer glimrende og har egentlig ingen store ulemper. Jeg oppnår det jeg vil, som altså er å hindre uvedkommende i å stjele båndbredden min.

Men det kan bli så mye bedre. Jeg savner nemlig et par ting. Nemlig å se hvilke som blir fanget opp er tungvint, og ikke minst kunne kontrollere tilgang raskt og effektivt basert på data i sanntid.

Eneste måten jeg kan se hvem som snylter på er nemlig å gå gjennom logger, for så å plukke ut de som synder mest (eller bruke fantastisk lang tid på å lete i loggene…). Og ettersom jeg ønsker å kunne se hvorvidt tiltaket virker (eller bare bidrar til gratis reklame for meg) er det ingen god løsning å lete i logger som kun viser fordeling på måned.

Men det var før. Nå har jeg gjort noen små endringer som fungerer enda bedre. Kan det fungere bedre enn glimrende sier du? Javisst. Følg med.

I den nye versjonen har jeg byttet ut loggfilen med et par tabeller i databasen min. En tabell styrer tilgang og en er for logg. htacces filen er endret til å sende alle forespørsler som ikke er fra eget domene eller et fåtall andre, deriblant bildesøk fra google, til et php script som tar seg kontroll og visning av bilder. Alt som som routes til scriptet logges med nødvendig info.

Tabellen som styrer tilgang har et flagg som forteller om det aktuelle domenet skal få tilgang til bildene. Om domenet ikke ligger i tabellen fra før blir det lagt til, med tilgang. Ønsker jeg å fjerne eller gi tilgang kan jeg når som helst endre dette. Verdien fra denne tabellen brukes for å vise det riktige bildet eller denne snasne dama.

Tabellen for logging inneholder hver eneste forespørsel som blir gjort med henvisende url, bildets url, domeneid koblet til tilgangstabellen, og hvorvidt tilgang er gitt. Dermed har jeg mulighet til å trekke ut det jeg trenger av statistikk. Og statistikk liker jeg, så det viser jeg med all data jeg har tilgengelig. Jeg har derfor statistikk fordelt på domener, bilder, tilgang, tidspunkt, i topplister og siste treff. IP og denslags logger jeg ikke.

Og hvis noen lurer på overskriften kan jeg fortelle at blogg.no står for brorparten av hotlinkere mot denne bloggen, sammen med et par forum hvor brukere har hotlinket avatar bildet sitt fra bloggen min. De bytter nok ganske snart…

Se forøvrig mer om hot linking her.

Oppdatering: Koden for php scriptet er lagt ut. Husk å endre filen til php, samt legge inn server, brukernavn, passord og database. Jeg benytter samme database som for WordPress, men med annet prefix for å skille tilhørighet for tabellene fra hverandre.

PHP (scriptet i filen du finner over):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
< ?php
$hotlink['hotlinkimage'] = 'donthotlink.jpg';
 
$hotlink['url'] = '';
if(!empty($_GET['url'])) {
	$hotlink['url'] = $_GET['url'];
 
 
	$hotlink['ref_domain'] = '';
	$hotlink['referrer'] = '';
	$hotlink['fullurl'] = 'http://'.$_SERVER['HTTP_HOST'].'/'.$hotlink['url'];
 
	// mod_rewrite should have already established that it is a hot-link.
	// This is a double-check. We need to capture the referrer, anyway.
 
	if (!empty($_SERVER['HTTP_REFERER'])) {
		$hotlink['referrer'] = $_SERVER['HTTP_REFERER'];
		$hotlink['ref_domain'] = substr($hotlink['referrer'], 7, strpos($hotlink['referrer'], "/", 7) - 7);
	}
 
	// splitting the referrer into a domain part, saves us searching for multiple variations.
	// so long as your host name appears somewhere in the domain part, it's not a hot-link.
 
	if (!stristr($hotlink['ref_domain'], $_SERVER['HTTP_HOST'])) {
 
		$domain = getDomain($hotlink['ref_domain']);
 
		if($domain->allow){
			log_hotlink_to_db($domain, $hotlink['referrer'], $hotlink['fullurl'], 1);
			serveImage('../'.$hotlink['url']);
		}
		else{
			log_hotlink_to_db($domain, $hotlink['referrer'], $hotlink['fullurl'], 0);
			serveImage($hotlink['hotlinkimage']);
		}
	}else{
		serveImage($hotlink['url']);
	}
 
}else{
	// No url? Redirect them to the main site.
	header( "Location: http://" . $_SERVER['HTTP_HOST'] );
}

En enkel klasse for domene. Kunne godt laget en klasse for referrer også, men er ikke nødvendig.

46
47
48
49
50
51
// Simple class for domain
class Domain{
	public $id;
	public $domain;
	public $allow;
}

Logge til db.

53
54
55
56
57
58
59
60
//log the attempt to db
function log_hotlink_to_db($domain, $referrer, $url, $allowed){
	$db_connection = getDBConnection();
	$statement = $db_connection->prepare("INSERT INTO hotlink_log (domain_id, referrer,url,allow) VALUES (?,?,?,?)") or die ("Failed to prepare the statement!");
	$statement->bind_param("issi", $domain->id, $referrer,$url,$allowed);
	$statement->execute();
	$statement->close();
}

Et par funksjoner for å håndtere domener.

62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
function getDomain($ref_domain){
	if (preg_match("/^www./i",$ref_domain)){
		// WWW. is deprecated anyway...
		$ref_domain = preg_replace("/^www./i", "", $ref_domain);
	}
 
	$domain = new Domain();
	$domain->id = 0;
	$domain->domain = $ref_domain;
	$domain = checkDomain($domain);
	if($domain->id == 0){
		insertNewDomain($domain);
		$domain = checkDomain($domain);
	}
	return $domain;
}
 
function checkDomain($domain){
	$db_connection = getDBConnection();
	$statement = $db_connection->prepare("SELECT id, allow FROM hotlink_access WHERE domain = ?") or die ("Failed to prepare the statement!");
	$statement->bind_param("s", $domain->domain);
	$statement->execute();
	$statement->bind_result($id, $allow);
	if($statement->fetch()){
		$domain->id = $id;
		$domain->allow = $allow;
	}
	$statement->close();
 
	return $domain;
}
 
function insertNewDomain($domain){
	$db_connection = getDBConnection();
	$statement = $db_connection->prepare("INSERT INTO hotlink_access (domain,allow) VALUES (?,1)") or die ("Failed to prepared the statement!");
	$statement->bind_param("s", $domain->domain);
	$statement->execute();
	//$statement->affected_rows
 
	$statement->close();
}

Vær så god, her har du et bilde. Håper du blir fornøyd.

104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
function serveImage($url){
	if (!empty($url) and file_exists($url)) {
		$hotlink['img_type'] = end(explode('.', $url));
		if (strcasecmp($hotlink['img_type'],'png') == 0 ){
			$hotlink['img'] = imagecreatefrompng($url);
			imagesavealpha($hotlink['img'],true);
		} elseif (strcasecmp($hotlink['img_type'],'jpg') == 0 || strcasecmp($hotlink['img_type'],'jpeg') == 0) {
			$hotlink['img'] = imagecreatefromjpeg($url);
		} elseif (strcasecmp($hotlink['img_type'],'gif') == 0) {
			$hotlink['img'] = imagecreatefromgif($url);
		} else {
			trigger_error("HOTLINK - Image $url is of unknown type", E_USER_ERROR);
		}
		//TODO Other file types?
 
	} else {
		// Log image not found!
		trigger_error("HOTLINK - Image $url was not found", E_USER_ERROR);
	}
 
	// send the image to the browser..
	if ($hotlink['img_type'] == 'png') {
		header('Content-type: image/png');
		imagepng($hotlink['img']) or die("there was an error. sorry about that...");
	} else {
		header('Content-type: image/jpg');
		imagejpeg($hotlink['img']) or die("there was an error. sorry about that...");
	}
	imagedestroy($hotlink['img']);
}

Skaff en connection til db. Det er her data om server, brukernavn, passord og database skal inn.

135
136
137
138
139
140
function getDBConnection(){
	$db_connection = new mysqli("server", "username", "password", "database") or die ("Failed to obtain connection to db!");
	return $db_connection;
}
 
?>

Databasetruktur (SQL) er som følger:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE TABLE hotlink_access (
  id INT(11) NOT NULL AUTO_INCREMENT,
  DOMAIN VARCHAR(50) NOT NULL,
  allow INT(11) NOT NULL,
  PRIMARY KEY  (id)
)
 
CREATE TABLE hotlink_log (
  id INT(11) NOT NULL AUTO_INCREMENT,
  domain_id INT(11) NOT NULL,
  referrer VARCHAR(200) NOT NULL,
  url VARCHAR(100) NOT NULL,
  allow tinyint(1) NOT NULL,
  TIME TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY  (id)
)

Egentlig er det også en timestamp i access tabellen også, men den trengs strengt tatt ikke da samme data ligger i loggtabellen.

Til slutt den delen som gjør selve redirekten: .htaccess filen:

1
2
3
4
5
6
7
8
# BEGIN Hotlink stopper
RewriteEngine On
RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^http(s)?://(www\.)?xmasb\. [NC]
RewriteCond %{HTTP_REFERER} !^http(s)?://((.+)\.)?google\.(.+imgres) [NC]
RewriteCond %{HTTP_REFERER} !^http(s)?://((.+)\.)?google\.(.+reader) [NC]
RewriteRule ^(.*)\.(gif|jpe?g|png)$ http://%{HTTP_HOST}/hotlink/hotlink.php?url=$1.$2 [R,NC,L]
# END Hotlink stopper

Alle request som ikke er direkte på bildene (gif|jpe?g|png), via xmasb, google’s bildesøk eller google reader blir sendt til scriptet. Scriptet sørger for resten. Her kan alle tillatte domener legges inn, så det ikke brukes unødig prossesering på tillatte domener. Eksempel på andre domener som kanskje bør ligge her er RSS lesere (Google, Bloglines, Netvibes f.eks) og andre sider for bildesøk. Det fine med denne fremgangsmåten er at det er lett å oppdage disse underveis, uten at de er utestengt i mellomtiden.

Det er lett å bare legge inn Google som tillatt domene i htaccess filen, men husk at det også eksisterer noe som heter sites.google.com. Scriptet får et par treff om dagen derfra.

Oppdatering 2: Har gjort en liten endring til på scriptet mitt. Ettersom blant annet blogg.no er kraftig overrepresentert i loggene er det like greit å sperre de ute med en gang. Jeg kunne selvsagt gjort dette via htaccess filen, men får å få med loggingen tar jeg de via scriptet likevel. For å gjøre dette har jeg gjort følgende endring i insertNewDomain funksjonen:

138
139
140
141
142
143
144
145
146
147
148
149
150
function insertNewDomain($domain, $allow = 1){
	global $hotlink;
	if(anyNeedleInString($domain->domain, $hotlink['blockedBaseDomains'])){
		$allow = 0;
	}
	$db_connection = getDBConnection();
	$statement = $db_connection->prepare("INSERT INTO hotlinkaccess (domain,allow) VALUES (?,?)") or die ("Failed to prepared the statement!");
	$statement->bind_param("si", $domain->domain, $allow);
	$statement->execute();
	//$statement->affected_rows
 
	$statement->close();
}

AnyNeedleInString ser slik ut:

187
188
189
190
191
192
193
194
function anyNeedleInString($haystack, $needle){
	foreach($needle as $key => $search_needle) { 
		if(stristr($haystack, $search_needle)) { 
			return true;
		}
	}
	return false;
}

Og så er det bare å definere en array som inneholder de domener man ikke vil ha med:

3
$hotlink['blockedBaseDomains'] = array('blogg.no', 'nettby.no');

Her illustrert med blogg.no og nettby.no. Dermed slipper jeg å håndtere de som tilhører diverse plattformer. Smart, ikke sant?