Навеяно статьёй про науку под замком. Хотя моя статья не об этом, но тоже про доступ к электронным библиотекам, хотя и другого характера.
Работаю в западном… не, всё же северном университете, и приходится читать немало статей из своей области исследований. Благо, тут университетская библиотека подписана на множество электронных библиотек (интересно, сколько это удовольствие стоит… не, не так – сколько буржуи наживаются на наших статьях?). По моей тематике таких библиотек три – ACM, IEEE и Springer. А в них – львиная доля того, что мне нужно. И всё бы здорово, но есть одно НО.
Электронные библиотеки (ЭБ) определяют пользователей, видимо, по IP. Может я не прав, но доступ к ним открыт только если компьютер физически (через витую пару) подключен к сети Университета. По крайней мере, на станицах ЭБ значится по чьей подписке я могу скачивать статьи. Достаточно только подключиться к сети Университета по беспроводной сети, как сразу же ЭБ начинает просить деньги за статью. Понятное дело, за пределами университета я только и вижу только тарифы на чтение труда коллег и своего собственного.
А что если я дома решил поработать (ну это я шучу: кто это сможет поработать в присутствии трёх сорванцов…) ну или в командировке мне позарез понадобилось что–то проверить, уточнить, или просто почитать, в конце–концов (очень реальный случай)? В какой–то момент, я решил надо как–то это дело решать. Местный админ обрадовал меня только тем, что я не первый кто просит у него придумать решение как получить доступ к ЭБ удалённо. Без обещаний что–либо сделать. Позже от коллеги узнал что есть какое–то решение на общенациональном ресурсе, но работает оно как–то нестабильно – несмотря не то что коллега показала как она подключается к ЭБ через этот ресурс, у меня почему–то те же действия не привели к желаемому результату.
Вот тогда и появилось решение сделать всё самому, поскольку имеются небольшие навыки программирования PHP и JS. А также тот факт, что есть папки пользователей в локальной сети (из–под которой действует подписка на ЭБ), и в них можно размещать PHP–скрипты. Задумка выглядела так:
- разместить прокси в своей папке на локальном сервере Университета,
- написать расширение под Google Chrome, чтобы
- все ссылки ведущие на страницах ЭБ «не туда» (не на PDF статьи) менялись бы «на лету», т.е. всё выглядело бы так как будто нет никакого прокси и статьи открываются так как будто работаешь из внутренней сети Университета.
После поиска PHP проксей был взят простенький скрипт (благодарности Eric-Sebastien Lachance) передающий все HTTP заголовки (т.е. включая куки пользователя). Правда, его пришлось весьма расширить, чтобы 1) ограничить число доменов к которым можно обращаться пользователю, и 2) разрешать пользоваться им только студентам и работникам Университета (ну вы понимаете почему это не может стать доступным для всех желающих...), а для этого проверять залогинен ли пользователь в локальной сети (Интранете, залогиниться туда можно удалённо; кстати, этот шаг можно опустить, если пользоватся только самому). Для этого в каждом запросе, кроме собственно URL на PDF статьи, должен присутствовать ID его сессии в Интранете. Прокси делает запрос на страницу Интранета, установив нужный куки (JSESSIONID в моём случае) в переданное значение, и если от Интранета приходит положительный ответ, то выполняет запрос к ЭБ.
function verifyUser($authCookieValue) {
global $authCheckURL;
global $authSuccessHTML;
global $authCookieName;
$result = false;
$error = '';
// create a new cURL resource
$ch = curl_init();
if ($ch !== false) {
// set URL and other appropriate options
$header = array();
$header[] = 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8';
$header[] = 'Accept-Language: en-US,en;q=0.5';
$header[] = 'Connection: close';
$header[] = 'DNT: 1';
curl_setopt($ch, CURLOPT_URL, $authCheckURL);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_USERAGENT, 'User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:17.0) Gecko/20100101 Firefox/17.0');
curl_setopt($ch, CURLOPT_COOKIESESSION, true);
curl_setopt($ch, CURLOPT_COOKIE, $authCookieName.'='.$authCookieValue);
curl_setopt($ch, CURLOPT_ENCODING, 'gzip,deflate');
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
// grab URL and pass it to the browser
$response = curl_exec($ch);
if($response === false) {
$error = curl_error($ch);
} else {
$result = strpos($response, $authSuccessHTML) !== false;
}
curl_close($ch);
} else {
$error = 'cannot initialize cURL';
}
return array(
'succeeded' => $result,
'error' => $error
);
}
На стороне пользователя работает расширение Google Chrome (состоит из двух частей – встраиваемый в страницу код, и код с доступом к вкладкам и куки работающий в фоновом режиме), и если URL относится к одной из трёх библиотек, то страница при загрузке сканируется на наличие определённых элементов которые должны быть на каждой странице где есть ссылка на PDF статьи (для каждой ЭБ это, конечно, разные элементы). Если такие элементы найдены, то туда подставляется запрос к прокси с правильно сформированной ссылкой на PDF статью. (Поиск того как и из каких из данных на странице правильно сформировать ссылку занял довольно немало времени).
// proxy config
var PROXY_URL = 'http://university.org/~user/proxy.php?';
var PROXY_URL_QUERY = 'urlForProxy=';
var PROXY_ID_QUERY = 'idForProxy=';
// page search and modification const
var ACM_PDF_LINK_NAME = 'FullTextPDF';
var ACM_ARTICLE_ID_NAME = 'id';
var ACM_PURCHASE_LINK = 'https://dl.acm.org/purchase.cfm';
var ACM_QUERY_URL = 'http://dl.acm.org/ft_gateway.cfm';
var ACM_QUERY = 'id={0}';
var ACM_LINK = '<a name="' + ACM_PDF_LINK_NAME + '" title="FullText PDF" href="{0}" target="_blank"><img src="imagetypes/pdf_logo.gif" alt="PDF" class="fulltext_lnk" border="0">PDF</a> [proxy]';
// requests to the background page
var REQUEST_AUTH = 'auth';
function setACMLink() {
var pdfLink = document.getElementsByName(ACM_PDF_LINK_NAME)[0];
if (!pdfLink) {
var i, id, param;
var params = window.location.search.substr(1).split('&');
for (i = 0; i < params.length; i++) {
param = params[i].split('=');
if (param[0] === ACM_ARTICLE_ID_NAME) {
id = param[1].indexOf('.') > 0 ? param[1].split('.')[1] : param[1];
break;
}
}
if (id) {
var link = PROXY_URL + ACM_QUERY.format(id) + '&' +
PROXY_URL_QUERY + encodeURIComponent(ACM_QUERY_URL);
// purchase link is a placeholder for a link to PDF
var a, container;
var links = document.getElementsByTagName('a');
for (i = 0; i < links.length; i++) {
a = links[i];
if (a.href.indexOf(ACM_PURCHASE_LINK) === 0) {
container = a.parentNode;
container.innerHTML = ACM_LINK.format('#');
setClick(container.childNodes[0], link);
break;
}
}
}
}
}
function setClick(elem, link) {
elem.addEventListener('click', function (e) {
commPort.postMessage({name: REQUEST_AUTH, href: link});
e.preventDefault();
return false;
});
}
При клике на это ссылку к запросу присоединяется значение куки которая содержит ID сессии в Интранете Университета.
// config
var AUTH_URL = 'https://university.org/intranet';
var AUTH_COOKIE = 'JSESSIONID';
// const
var REQUEST_AUTH = 'auth';
chrome.runtime.onConnect.addListener(function (port) {
port.onMessage.addListener(function (request) {
var answer = {toRequest: request.name};
if (request.name === REQUEST_AUTH) { // check the authorization on the select web-site
answer.href = request.href;
answer.result = false;
answer.id = '';
chrome.cookies.get({url: AUTH_URL, name: AUTH_COOKIE}, function (cookie) {
if (cookie) {
answer.result = true;
answer.id = cookie.value;
}
port.postMessage(answer);
});
}
});
});
Во встроеном в страницу коде:
var commPort = chrome.runtime.connect();
commPort.onMessage.addListener(function (answer) {
if (answer.toRequest === REQUEST_AUTH) {
// add an authorization id, and send the request to to the proxy
window.location = answer.href + '&' + PROXY_ID_QUERY + answer.id;
}
});
Правда, для того чтобы открывались статьи на IEEE пришлось немало повозиться. Оказывается, для IEEE нужно открыть любую страницу на сайте его ЭБ через прокси (в Хроме открывается дополнительная вкладка, которая закрывается сразу после загрузки контента), чтобы получить от него куки с идентификаторами пользователя с подпиской. Плюс пришлось поколдовать с заменой в полученных куки значений domain и path (последнее было необязательным) в PHP, чтобы они могли автоматически передаваться вместе с запросом на PDF к прокси: приходили они (после этого дополнительного запроса для получения идентификаторов) как domain=ieeexplore.ieee.org, а линк на PDF уже указывал не на .ieeexplore.ieee.org/query, а на university.org/~user/proxy?url= ieeexplore.ieee.org%5Fquery, поэтому надо было править их на domain=.university.org, path=/~user/proxy.
// config
$cookieDomain = '.university.org';
$cookiePath = '/~user';
$headerArray = explode("rn", $response['header']);
$js = '';
foreach ($headerArray as $headerLine) {
if (strpos($destinationURL, 'ieee.org') !== false) {
if (strpos($headerLine, 'Set-Cookie: ') !== false) {
$cookieArray = explode(': ', $headerLine, 2);
$headerLine = $cookieArray[0].': ';
$cookieDataArray = explode('; ', $cookieArray[1]);
$isFirstKey = true;
$js .= ' document.cookie = "';
foreach ($cookieDataArray as $cdKey => $cookieData) {
list($cname, $cvalue) = array_merge(explode('=', $cookieData), array(''));
if ($cname === 'domain') {
$cvalue = $cookieDomain;
$cookieDataArray[$cdKey] = $cname.'='.$cvalue;
}
if ($cname === 'path') {
$cvalue = $cookiePath;
$cookieDataArray[$cdKey] = $cname.'='.$cvalue;
}
$headerLine .= ($isFirstKey ? '' : '; ').$cookieDataArray[$cdKey];
$js .= ($isFirstKey ? '' : '; ').$cookieDataArray[$cdKey];
$isFirstKey = false;
}
$js .= "";rn";
}
header($headerLine);
if (strlen($js) > 0) {
echo "rn<script>rn".$js.'</script>'; // insert JS code into the page to set IEEE session and identification cookies
}
} else {
foreach ($headerArray as $headerLine) {
header($headerLine);
}
}
}
Последнее что хочу отметить, так это то это слабые стороны такой реализации:
- Если поменяется что–то в вёрстке страниц ЭБ, это решение перестанет работать. Зато пока работает это очень удобно: залогинился в Интранете, и работаешь
- Есть кое–какие проблемы с безопасностью: идентификатор сессии передаётся по HTTP, и потенциально его можно перехватить. Пока я один использую это решение, риск мне видится нулевым – как злоумышленник узнает что за набор цифр я передаю в прокси? Однако если он изучит расширение, получение доступа к чужому аккаунту в Интранете Университета станет делом техники. А HTTPS на директории пользователей на нашем сервере не распространяется… Возможно, надо будет использовать шифрование идентификатора сессии в расширении (CryptoJS) и расшифровку на стороне сервера (пока не нашёл, как: буду признателен за подсказку), так как всё же планирую разослать расширение коллегам
А вот полный исходный код.
P.S. Если кто–то знает лучшее и такое же удобное решение, просьба поделиться. Можно брать код, пользовать, редактировать, делиться и т.д. Вообще, есть идея это на github залить, и если будут пожелания в комментариях, то так и сделаю.
P.P.S. За код сильно не ругайте — времени больше ушло на выяснение какие слать запросы проски (особенно для ЭБ на IEEE), чем на сам код. Он может также неполным — не всегда подставлять правилный линк.
Автор: lexasss