Заниматься чем-то, нарушающим священное волеизъявление здешних господ на контент, который и только который они хотят видеть вокруг себя — дело, конечно, неблагодарное и кармически опасное. Но гонки вооружений между блокировщиками рекламы и рекламными системами не избежать, поэтому говорить об этом нужно. Сейчас, когда общий объем вырезанного трафика крутится около 1% — всё несколько вяленько, но уже есть оглядывающиеся владельцы сайтов, недополучающие до 30% денег с рекламы. Рекламные сети начинают общаться между собой, обмениваться спецификациями, есть уже какой-то израильский стартап на эту тему — думаю, соблазнительно с минимальными усилиями увеличить доход сразу и на проценты. В российском сегменте всё пока обходится увещевательными объявлениями вида «Вы отключили рекламу — это мешает нам развиваться» или простым игнорированием факта существования таких пользователей. Надо сказать, пусть оно всё так и остается.

Здесь, исключительно в режиме минимального доказательства работоспособности — будем обходить самый распространенный тип блокировщиков рекламы — по паттерну URL. Метод должен поддерживать:

  • хранения cookie рекламных систем на стороне пользователя
  • передавать не меньшее количество информации о пользователя, чем браузер: User-Agent, IP
  • требовать минимальной настройки большинства стандартных рекламных тэгов
  • быть легко подключаемым и изменяемым для случаев, когда кто-то не поленился и всё-таки добавил кастомное правило, попавшее в мейнстрим

Для достижения результата — будем маскировать все URL рекламных сетей через своеобразное прокси между сервером издателя и рекламодателя. Метод не самый дешевый c точки зрения ресурсов, потенциально опасен и для рекламодателей: источник фрода, не обойтись без оценкци качества трафика через клики или конверсии, и для издателей — если забанит гугл, то забанит весь сайт. Соответственно требует некоторого порога доверия между ними, но ��то всё отдельные вопросы. Да, все совпадения или упоминания случайны, брал то, что под руку попало.

Итак, мы хотим все вызовы сайта для тех у кого детектированы блокировщики рекламы, вида
ads.*.ru/228129/prepareCode?pp=g&ps=bugf&p2=ezfl&pct=a&plp=a&pli=a&pop=a'


Заменить на локальные, неотличимые от свойственного сайту контента, например

/meduza/2015/09/28/shapitorbFgFQ4Y7_Z3jaPSRix09Pn5VyrnV5Pcki64JcXvjIyzlgYXerh3yMMgY8DB5vleYAkS_gbRiHDSyQHU7QscAd38-1tKyYnnLjLSlpHq6aJ4sEo


Для наглядности начну с того, что будет нужно.

1. Взять рекламный тэг, зашифровать первую точку входа (та, которая в самом рекламном тэге) вручную

<!--Тип баннера: 990x90js-->
<!--Расположение: <верх страницы>-->
<script type="text/javascript">
    <!--
    (function(){
         //var link = 'http://ads.XXXXXX.ru/228129/prepareCode?pp=g&ps=bugf&p2=ezfl&pct=a&plp=a&pli=a&pop=a',
         var link = '/meduza/2015/09/28/shapitorbFgFQ4Y7_Z3jaPSRix09Pn5VyrnV5Pcki64JcXvjIyzlgYXerh3yMMgY8DB5vleYAkS_gbRiHDSyQHU7QscAd38-1tKyYnnLjLSlpHq6aJ4sEo',
         
     params = 'phone';
     new AXXXBanner(link, params).createBanner();
     })();
    //-->
</script>

2. Чуть расширить конфигурацию сервера (в данном случае nginx), начав обрабатывать такие урлы в особом режиме и отдавая обработку запроса на откуп скрипту:

    location /meduza/2015/09/28/shapito {
        set $prefix "http://localhost:9090/meduza/2015/09/28/shapito";
        rewrite shapito(.*)$ /adbb.php?query=$1&url_mask_prefix=$prefix&ua=$http_user_agent&remote_addr=$remote_addr;
    }

Всё — после этого реклама будет показана.

Как это работает.

Отступление, выбор средств
Хотел написать всю логику на Lua, но было лень пересобирать nginx и, так как PHP потенциально ближе к сайтовладельцам — ис��ользовал его. Код тривиальный писался для proof-of-concept, в две страницы, можно воспроизвести на любом языке — приложен в конце. Есть зависимость от mbcrypt и curl.

Алгоритм работы.

1. Расшифровать переданную строку ключом из конфигурации, получив целевой URL

2. Взять оттуда домен, зашифровать его

ads.XXXXXX.ru -> pPM9l7raWppVawqO


3. По этому ключу поищем в присланных нам cookie, найдем значение вида

niFJ2HLxzm27hCLnQUvcmLx62sEU-worI4tjmSAfqxNSMR6DSZ279lampNh_CN2jlu7FXaVk0WRVt-HMxy4vdm0uEncngawC6RvcKBwRXrT0wIi0icl4BvSXPJzH99C_5-mTmneEISfz


И расшифруем его, получив исходные куки, когда-то присланные нам от этого сервера для установки.

4. Если URL содержит маркеры клика, не делаем больше ничего — просто отдаем 302 редирект и выходим.

5. Иначе делаем запрос на целевой URL, передавая cookie, исходный пользовательский User-Agent и реальный IP в X-Forwarded-For заголовке.

6. Анализируем ответ от сервера:

5.1. Если были присланы Set-Cookie — перепаковываем их (ключ — зашифрованный домен, значение — зашифрованное Set-Cookie из результатов вызова), и устанавливаем их, получится что-то вроде

Set-Cookie:oI5upmClXJaq6DY4QWT5g5ZsvQ=niFJ3pLNsqSh0x19ux1HB-3XQiMb3XDhuJC5Byrefm_xOIDJBlZ2FL5q2zvyVtPcNOimtTk-lfoY; expires=Mon, 28-Dec-2015 08:23:57 GMT; Max-Age=7776000


5.2. Если content-type не содержит text или javascript — например, изображения, gif-пиксели — отдаем контент as-is, настроив кэширование по вкусу (но лучше не надо). И выходим

5.3. Для всех остальных content-type ищем в теле ответа URL по паттерну, шифруем каждый, и используя переданный от сервера префикс, заменяем вызовы на замаскированные, то есть, места вида

object_1986381203 += '<a href="http://ads.XXXXX.ru/228129/goLink?pr=gleurtb&p5=dcxnh&p1=bohgi&p2=ezfl" target="_blank"><img src="http://content.XXXXX.ru/150924/XXXX/507850/1421973.jpg" width="990" height="90" alt="" border=0></a>';

Будут отданы так:

object_1986381203 += '<a href="/meduza/2015/09/28/shapitorbFgFQ4Y7_Z3jaPSRix09Pn5VyrnV5P" target="_blank"><img src="/meduza/2015/09/28/shapitorbFgFQ4Y7_Z3jaPSRix09Pn5VyrnV5Pcki64JcXvjIyzlgYXerh3yMMgY8DB5vleYA" width="990" height="90" alt="" border=0></a>';

Отдаем измененный контент и выходим.

6. Браузер сам сделает нужные вызовы, для каждого из которых будет пройдено всё с п.1

Секция конфигурации скрипта

$CONF = array(
	'mask_urls' => array(
		'prefix' => '/meduza/2015/09/28/shapito', //default, will be overriden by `url_mask_prefix`
		'url_search_patterns' => array(
			'@https?://[^\\\'\"\n\r\?]+@i',
		),
	),

	'redirect_if_contains' => array(
		'/click', '/reference', '/link', '/goLink'
	),

	'encrypt' => array (
		'iv' => '2uV17Dil',
		'key' => 'JbaSyaXwD46qIlKdt8mJ4',
		'cipher' => MCRYPT_GOST, 
		'mode' => MCRYPT_MODE_CFB
	),

	'cookie' => array(
		'expire' => 90*24*60*60,
	),

	'url_call' => array(),
);

Ещё раз, это концепт — использовать его в продакшн нельзя! Он сделает из вашего сервера анонимный прокси

adbb.php
<?php 

$CONF = array(
	'mask_urls' => array(
		'prefix' => '/meduza/2015/09/28/shapito', //default could be taken from `url_
		'url_search_patterns' => array(
			'@https?://[^\\\'\"\n\r\?]+@i',
		),
	),

	'redirect_if_contains' => array(
		'/click', '/reference', '/link', '/goLink'
	),

	'encrypt' => array (
		'iv' => '2uV17Dil',
		'key' => 'JbaSyaXwD46qIlKdt8mJ4',
		'cipher' => MCRYPT_GOST, 
		'mode' => MCRYPT_MODE_CFB
	),

	'cookie' => array(
		'expire' => 90*24*60*60,
	),

	'url_call' => array(),
);

require_once __DIR__."/adbb_functions.php";

if (   !array_key_exists('query', $_REQUEST) or !$query = $_REQUEST['query']) return;


adbb_debug_log('Request params', $_REQUEST);

$extra_query = '';
if (($delpos = strpos($query, '&')) !== false) {
	adbb_debug_log('Extra parameters passed, getting encypted part only. @ position ', $delpos);	
	$extra_query = substr($query, $delpos);
	adbb_debug_log('Extra query ', $extra_query);	
	$query = substr($query, 0, $delpos);
	adbb_debug_log('Encrypted query after cut ', $query);	
}

adbb_debug_log('We should have successfully decrypted url with the key for all further logic, decrypting...');

if (  !$url = adbb_decrypt($query, $CONF['encrypt']) )   { 
	adbb_debug_log('Failed. Exiting');	
	return;	
}

adbb_debug_log('Decrypted URL', $url);

if (array_key_exists('url_mask_prefix', $_REQUEST)) {

	adbb_debug_log('Overriding initial url_mask_prefix ['.$CONF['mask_urls']['prefix'].'] to', $_REQUEST['url_mask_prefix']);
	$CONF['mask_urls']['prefix'] = $_REQUEST['url_mask_prefix'];

}

$extra_args = $_REQUEST;
unset($extra_args['query'], $extra_args['ua'], $extra_args['remote_addr'], $extra_args['url_mask_prefix']);

$url_to_call = $url . (strpos($url, '?') === false ? '?' : '&' ) . http_build_query($extra_args).'&'.$extra_query;


adbb_debug_log('Looking for cookies to send to remote host...');
$domain_cookies = ''; 
$parse = parse_url($url_to_call);
$domain_to_call = $parse['host'];

if ($domain_to_call) {
	$domain_cookie_key = adbb_encrypt($domain_to_call, $CONF['encrypt']);
	
	adbb_debug_log('Will look for cookies for domain ['.$domain_to_call.'], key', $domain_cookie_key);
	if (isset($_COOKIE) and array_key_exists($domain_cookie_key, $_COOKIE) and $_COOKIE[$domain_cookie_key]) {

		adbb_debug_log('Found something, will try to decrypt cookie value ', $_COOKIE[$domain_cookie_key]);
		if ($json_encoded_cookies = adbb_decrypt($_COOKIE[$domain_cookie_key], $CONF['encrypt'])
			and $cookies = @json_decode($json_encoded_cookies) ) {

			adbb_debug_log('Going to send cookies decrypted', $cookies);
			$domain_cookies = implode('; ', $cookies);

		}
	}
}

adbb_debug_log('Checking if redirect is needed...');

foreach ($CONF['redirect_if_contains'] as $needle) {

	if (strpos($url_to_call, $needle) !== false ) {

		$url_to_call .= '&cookies='.urlencode($domain_cookies);
		adbb_debug_log('URL contains '.$needle.' going to redirect to ', $url_to_call);
		
		header('Location: '.$url_to_call);
		exit;
	}
}

adbb_debug_log('About to call remote URL ', $url_to_call);
adbb_debug_log('With User-Agent ', $_REQUEST['ua']);
adbb_debug_log('With X-Forwarded-For ', $_REQUEST['remote_addr']);
adbb_debug_log('With Cookie  ', $domain_cookies);

$result = adbb_call_url(
	$url_to_call, '', $_REQUEST['ua'], 
	array(
		'X-Forwarded-For' => $_REQUEST['remote_addr'],
		'Cookie' => $domain_cookies
	)
);

if ($result) {
	adbb_debug_log('Call successfull.');		
	$info = $result['info'];
	$header = $result['header'];
	$content = $result['content'];

	adbb_debug_log('Remote response info', $info);
	adbb_debug_log('Remote response headers', $info);

	$parse = parse_url($url);
	$domain = $parse['host'];

	if (array_key_exists('Set-Cookie', $header[count($header)-1])) {

		adbb_debug_log('Found Set-Cookie in response, taking last one');
		$domain_set_cookies = $header[count($header)-1]['Set-Cookie'];

		$domain_cookies = adbb_translate_cookie_values($domain_set_cookies);
		adbb_debug_log('Translated domain cookies', $domain_cookies);

		$json_encoded_cookies = json_encode($domain_cookies);
		adbb_debug_log('Json encoded cookies', $json_encoded_cookies);

		$encrypted_cookies_domain = adbb_encrypt( $domain, $CONF['encrypt'] ) ;
		$encrypted_cookies_values = adbb_encrypt( $json_encoded_cookies, $CONF['encrypt'] ) ;
		adbb_debug_log('About to set encrypted cookie for domain ['.$domain.'] as ['.$encrypted_cookies_domain.'] with value ', $encrypted_cookies_values);

		setcookie($encrypted_cookies_domain, $encrypted_cookies_values, time()+$CONF['cookie']['expire']);

	}

	if (strpos($info['content_type'], 'text') === false and strpos($info['content_type'], 'javascript') === false ) {

		adbb_debug_log('Non-text content type '.$info['content_type'].', passing as is. Cache headers not implemented');
		header('Content-Type: '.$info['content_type']);
		echo $content;

	} else {

		adbb_debug_log('Initial content in remote response', $content);
		$new_content = adbb_mask_urls($content, $CONF['mask_urls'], $CONF['encrypt']);
		echo $new_content;	
	}

}



adbb_functions.php
<?php 

function adbb_encrypt($data, $encrypt_conf) {
	$iv = $encrypt_conf['iv'];

  	return rtrim(strtr(
  			base64_encode(mcrypt_encrypt($encrypt_conf['cipher'], $encrypt_conf['key'], $data, $encrypt_conf['mode'],$iv)),
  			 '+/', '-_'),
  		'='); 

} 

function adbb_decrypt($data, $encrypt_conf) {

	$encrypted = base64_decode(str_pad(strtr( $data, '-_', '+/'), strlen( $data ) % 4, '=', STR_PAD_RIGHT));

	return mcrypt_decrypt(
		$encrypt_conf['cipher'], $encrypt_conf['key'], $encrypted, $encrypt_conf['mode'], $encrypt_conf['iv']
	);
} 

function adbb_translate_cookie_values($set_cookies) {
	$keys = array();
	foreach ($set_cookies as $sc)   {
		if ($pos = strpos($sc, ';')) {
			$keys[] = substr($sc, 0, $pos);
		} else {
			$keys[] = $sc;
		}
	}

	return $keys;
}

function adbb_get_headers_from_curl_response($headerContent)
{

    $headers = array();

    // Split the string on every "double" new line.
    $arrRequests = explode("\r\n\r\n", $headerContent);

    // Loop of response headers. The "count() -1" is to 
    //avoid an empty row for the extra line break before the body of the response.
    for ($index = 0; $index < count($arrRequests) -1; $index++) {

        foreach (explode("\r\n", $arrRequests[$index]) as $i => $line)
        {
            if ($i === 0)
                $headers[$index]['http_code'] = $line;
            else
            {
                list ($key, $value) = explode(': ', $line);
                if (array_key_exists($key, $headers[$index])) {
                	if (is_array($headers[$index][$key])) {
                		$headers[$index][$key][] = $value;
                	} else {
                		$t = $headers[$index][$key];
                		$headers[$index][$key] = array( $t );
                		$headers[$index][$key][] = $value;

                	}
                } else {
                	$headers[$index][$key] = $value;	
                }
            }
        }
    }

    return $headers;
}

function adbb_call_url($url, $cookie, $ua, $headers) {
	$ch = curl_init();

	$curlopt_headers = array();

	foreach ($headers as $k => $v) $curlopt_headers[] = $k.': '.$v;

	$options = array(
		CURLOPT_URL => $url, 
		CURLOPT_RETURNTRANSFER => true,
		CURLOPT_FOLLOWLOCATION => true,
		CURLOPT_USERAGENT => $ua,
		CURLOPT_CONNECTTIMEOUT => 5,
		CURLOPT_TIMEOUT => 5,
		CURLOPT_MAXREDIRS => 5,
		CURLOPT_SSL_VERIFYHOST => 0,
		CURLOPT_COOKIE => $cookie,
		CURLOPT_HTTPHEADER => $curlopt_headers,

		CURLOPT_VERBOSE => 1,
		CURLOPT_HEADER => 1,
	);

	curl_setopt_array($ch, $options);

	$response = curl_exec($ch);

	if (!$response) return false;

	$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
	$header = adbb_get_headers_from_curl_response(substr($response, 0, $header_size));
	$content = substr($response, $header_size);


	$info = curl_getinfo($ch); 

	return array(
		'header' => $header,
		'content' => $content,
		'info' => $info 
	);

}

function adbb_mask_urls($content, $mask_conf, $encrypt_conf) {
	$prefix = $mask_conf['prefix'];

	$url_search_patterns = $mask_conf['url_search_patterns'];

	foreach ($url_search_patterns as $pattern) {
		$matches = array();
		if (preg_match_all($pattern, $content, $matches)) {
			adbb_debug_log('Url matches ',$matches);
			foreach ($matches[0] as $m) {
				$encrypted_url = adbb_encrypt($m, $encrypt_conf);
				$content = str_replace($m, $prefix.$encrypted_url, $content);
			}
		}
	}

	return $content;
}


function adbb_debug_log($message, $obj = false) { 
	// $s = $message.($obj ? ' ( '.(is_string($obj) ? $obj : var_export($obj,true) ) . ' )' : '' )."\n";
	// static $f = null; 
	// if (!$f and $f = @fopen("/tmp/adbb_tmp_log".date("Ymd"),"a+")) {
	// 	fputs($f, "== New call ==");
	// }
	// if ($f) {
	// 	fputs($f, $s);
	// }
	//echo  $s ;  
} 



Для получения самой ссылки первичного входа, выполните с той же конфигурацией

adbb_encrypt('http://ads.XXX.ru/XXXX/prepareCode?pp=g&ps=bugf&p2',  $CONF['encrypt']);


Спасибо за внимание.