Десять примеров того, как не нужно писать PAC-файлы

betep37 10 мая в 12:37 2,7k

image


Введение


Практически любое внедрение Web Security Gateway, будь то облачное SaaS решение, вроде Zscaler или on-premises appliance, такое, как Cisco WSA (IronPort), не обходится без конфигурирования прокси-серверов в браузерах при определенных кейсах и потому по работе мне часто приходится сталкиваться с файлами авто-конфигурации прокси-серверов (PAC, proxy auto configuration). В этой статье я бы хотел рассмотреть несколько примеров оптимизации их производительности.


Зачем нужна эта статья


Почему я решил написать эту статью и есть ли в ней польза? Надеюсь, что да, и вот почему. По сути, PAC-файл есть JavaScript функция поиска соответствия строки/подстроки полей url/host, которая возвращает имя прокси сервера для ресурса или предписывает браузеру использовать прямой доступ к ресурсу в обход прокси. Как и любой язык программирования, код JavaScript также можно оптимизировать для выполнения. В условиях крупных компаний/предприятий со сложной, распределенной инфраструктурой доступа к сети Интернет и, как следствие, PAC-файлами, состоящими из нескольких сотен строк кода, задача по оптимизации PAC-файлов уже не кажется чем-то абсолютно бесполезным, так как, например, проценты времени исполнения одной не оптимальной или используемой не к месту функции, очевидно, будут помножены на количество её вхождений (применений) её в коде.


Далее под катом.



Disclaimer


Эта статья не ставит своей целью развернуто описать функции, применяемые в PAC-файлах. Для более подробного описания каждой функции я рекомендую обратиться за справкой, например, к сайту http://findproxyforurl.com. Результаты тестов производительности различных браузеров в этой статье могут быть спорными и ни в коем случае не претендуют на абсолютную истину, однако, автор старался приложить максимум усилий, чтобы достичь правдивого результата. Тем не менее, по обозначенной причине спорности, сводные результаты тестов не приводятся — читателю предлагается самостоятельно убедиться в преимуществе использования тех или иных подходов. Для этого там, где возможно, будут даны ссылки на тесты. В своём изыскании я в первую очередь полагался на ресурс jsperf.com, который очень кстати позволяет при наличии аккаунта на github создавать сценарии тестирования и замерять производительность кода на JavaScript, именно там будут храниться сценарии тестов к этой статье.


Ну что же, перейдем к делу.


Пример первый


Первым в моём списке идёт наиболее простой случай, когда требуется выбрать прокси для строго определенного хоста. Очень часто коллеги зачем-то используют в таком случае проверку url не к месту или обрамляют строку «*». Часто такое бывает, когда в PAC-файле уже за сотню строк и кто-то добавляет по аналогии новое условие вроде следующего:


else if (shExpMatch(url,"*cisco.com*")) return 'PROXY  MyProxy1:3128';

или так все же с host:


else if (shExpMatch(host,"cisco.com")) return ‘PROXY  MyProxy1:3128';

Проблема здесь даже не в том, что для Chrome 58 и IE11 первый вариант почти на 1% медленнее (для Firefox 53 между ними почти нет разницы), самое зло здесь в том, как, надеюсь, многие из читателей догадались, что для URL, например, «http://www.noncisco.com/evil/cisco.com» оба результата вернут true. Решение проблемы очень простое — не использовать функцию shExpMatch, которая для этого не предназначена, а вместо неё искать точное совпадение:


   if (host == 'www.cisco.com') return 'PROXY  MyProxy1:3128';

Минус здесь также очевиден. Код выше вернёт false для http://cisco.com, хотя, конечно, никто не мешает вам сделать две проверки:


   if ((host == 'www.cisco.com') || (host == 'cisco.com')) return 'PROXY  MyProxy1:3128';

Собственно, ссылка на тест, для желающих убедиться в производительности: https://jsperf.com/inefficient-shexpmatch


К слову, а как тестировать PAC-файлы? Не перебирать же их вручную в браузере в самом деле? Хотя этот способ и будет ультимативным, я все же использую для этих целей ресурс http://home.thorsen.pm/proxyforurl, который меня ещё не подводил и не обманывал.


Пример второй


Проблема номер два очень близка по своей сути к тому, что мы рассмотрели выше, и здесь опять применяют нелюбимую мной функцию по-умолчанию shExpMatch.
Задача — для определенного домена использовать MyProxy2. Что делают:


if (shExpMatch(host, "*.linkedin.com")) return 'PROXY  MyProxy2:3128';

Опять же не оптимально, не точно, не к месту. Используем функцию


if (dnsDomainIs(host,".linkedin.com")) return 'PROXY  MyProxy2:3128';

Хотя, строго говоря, для браузеров FF и Chrome эти функции почти равны по производительности, для IE11 это существенно и разница более чем в 1%.
Собственно, тест:
https://jsperf.com/dnsdomainis-vs-shexpmatch


Пример третий


Проблемка номер три, которая часто возникает из-за невнимательности администраторов, заключается она в дублирующих друг друга условиях. Пример:


else if (shExpMatch(host, "192.168.*")) return "DIRECT";
else if (shExpMatch(host, "10.*.*.*")) return "DIRECT";
…
else if (isInNet(host, "192.168.88.0", "255.255.255.0")) return "DIRECT";
…
else if (shExpMatch(url, "*10.10.*")) return "DIRECT";

Мало того, что условия дублируют (перекрывают) друг друга, так ещё они и не отработают в принципе, если пользователь ввёл доменное имя, а не ipv4 адрес в адресную строку браузера. Решение — исключить повторяющиеся условия и выполнить разрешение имени, хотя бы так:


else if (shExpMatch(dnsResolve(host), "192.168.*")) return "DIRECT";
else if (shExpMatch(dnsResolve(host), "10.*.*.*")) return "DIRECT";

Пример четвертый


Собственно, проблема номер четыре является развитием того, что описано в предыдущем разделе. Положим, администратор желает для всех подсетей из диапазона RFC1918 направить клиентов в обход прокси и делает так:


…
else if (shExpMatch(dnsResolve(host), "192.168.*")) return "DIRECT";
else if (shExpMatch(dnsResolve(host), "10.*.*.*")) return "DIRECT";
else if (shExpMatch(dnsResolve(host), "172.16.*.*")) return "DIRECT";
else if (shExpMatch(dnsResolve(host), "172.17.*.*")) return "DIRECT";
else if (shExpMatch(dnsResolve(host), "172.18.*.*")) return "DIRECT";
else if (shExpMatch(dnsResolve(host), "172.19.*.*")) return "DIRECT";
else if (shExpMatch(dnsResolve(host), "172.20.*.*")) return "DIRECT";
else if (shExpMatch(dnsResolve(host), "172.21.*.*")) return "DIRECT";
else if (shExpMatch(dnsResolve(host), "172.22.*.*")) return "DIRECT";
else if (shExpMatch(dnsResolve(host), "172.23.*.*")) return "DIRECT";
else if (shExpMatch(dnsResolve(host), "172.24.*.*")) return "DIRECT";
else if (shExpMatch(dnsResolve(host), "172.25.*.*")) return "DIRECT";
else if (shExpMatch(dnsResolve(host), "172.26.*.*")) return "DIRECT";
else if (shExpMatch(dnsResolve(host), "172.27.*.*")) return "DIRECT";
else if (shExpMatch(dnsResolve(host), "172.28.*.*")) return "DIRECT";
else if (shExpMatch(dnsResolve(host), "172.29.*.*")) return "DIRECT";
else if (shExpMatch(dnsResolve(host), "172.30.*.*")) return "DIRECT";
else if (shExpMatch(dnsResolve(host), "172.31.*.*")) return "DIRECT";
…

Само собой, напрашивается один раз провести разрешение имени и далее уже работать с переменной, в которой будет записан результат:


var resolved_ip = dnsResolve(host);
...
else if (shExpMatch(resolved_ip, "192.168.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "10.*.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.16.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.17.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.18.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.19.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.20.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.21.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.22.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.23.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.24.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.25.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.26.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.27.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.28.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.29.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.30.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.31.*.*")) return "DIRECT";
...

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


Пример пятый


Продолжаем нашу оптимизационную рекурсию. Блок кода из предыдущей проблемы уже из-за своего размера напрашивается к оптимизации. Допустим, изначально функция выглядела так:


function FindProxyForURL(url, host)
{

var resolved_ip = dnsResolve(host);

if (shExpMatch(resolved_ip, "127.*.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "192.168.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "0.*.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "10.*.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.16.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.17.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.18.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.19.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.20.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.21.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.22.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.23.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.24.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.25.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.26.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.27.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.28.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.29.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.30.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "172.21.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "169.254.*.*")) return "DIRECT";
else if (shExpMatch(resolved_ip, "192.88.99.*")) return "DIRECT";
...
}

Даже если оптимизировать код выше и использовать вместо else if логическое или (||), по результатам тестов среди трех уже упомянутых браузеров, наиболее оптимальной оказалось задействовать проверку регулярного выражения следующим образом:


function FindProxyForURL(url, host)
{
        var privateIP = /^(0|10|127|192\.168|172\.1[6789]|172\.2[0-9]|172\.3[01]|169\.254|192\.88\.99)\.[0-9.]+$/;

var resolved_ip = dnsResolve(host);

        if (privateIP.test(resolved_ip)) {
                        return "DIRECT";
        }
}

Заметьте, это не только эффективнее, но и выглядит компактнее и изящнее. Но нет предела совершенству, и если воспользоваться специализированной функцией IsInNet, то можно добиться ещё более впечатляющих результатов. По сравнению с предыдущим примером, код ниже даёт прирост производительности от 0,2 до 2% в различных браузерах:


function FindProxyForURL(url, host)
{

var resolved_ip = dnsResolve(host);

if ((isInNet(resolved_ip , "192.168.0.0", "255.255.0.0")) || (isInNet(resolved_ip , "172.16.0.0", "255.240.0.0")) || (isInNet(resolved_ip , "10.0.0.0", "255.0.0.0")) || (isInNet(resolved_ip , "127.0.0.0", "255.0.0.0")) || (isInNet(resolved_ip , "0.0.0.0", "255.0.0.0")) || (isInNet(resolved_ip , "169.254.0.0", "255.255.0.0")) || (isInNet(resolved_ip , "192.88.99.0", "255.255.255.0"))) return "DIRECT";

}

Здесь можно повторить сам тест: https://jsperf.com/privateip-test-vs-shexpmatch


Пример шестой


Небольшое отступление от основной линии для разнообразия. Рассмотрим две строки кода ниже:


dnsDomainIs("www.notmycompany.com", "mycompany.com")
dnsDomainIs("www.MyCompany.com", "mycompany.com")

Если первая строка вернет ожидаемо true (да, да, именно true, это всего лишь поиск подстроки!), то вторая строка, несмотря на кажущееся точное соответствие, из-за разницы в прописных буквах вернет false (к слову, вроде как в FF до версии 52 результат был бы все же true, но автор вычитал это где-то на просторах сети и не проверял это самолично).


Таким образом, для решения проблемы крайне желательно в самом начале PAC-файла выполнить преобразование строки url и host, если далее мы работаем с ними, например, так:


var url_lc = url.toLowerCase();
var host_lc = host.toLowerCase();

И далее работать только с url_lc и host_lc.


Пример седьмой


Очень простая проблема, но от того не менее распространенная. Как и многие проблемы, возникает по невнимательности, пример ниже взят из реального кейса внедрения, но имена, конечно, вымышленные и любые совпадения являются чистой случайностью:


...
else if (shExpMatch(host, "files.company.ru")) return "PROXY companyproxy7:3128";
else if (shExpMatch(host, "mail.company.ru")) return "PROXY companyproxy7:3128";
else if (shExpMatch(host, "company.com")) return "PROXY companyproxy7:3128";
else if (shExpMatch(url, "*downloads*.company.com*")) return "PROXY companyproxy7:3128";
else if (shExpMatch(host, "global.company.com")) return "PROXY companyproxy7:3128";
else if (shExpMatch(host, "db.company.com")) return "PROXY companyproxy7:3128";
else if (shExpMatch(host, "test.company.com")) return "PROXY companyproxy7:3128";
else if (shExpMatch(host, "blog.company.com")) return "PROXY companyproxy7:3128";
else if (shExpMatch(host, "servicedesk.company.com")) return "PROXY companyproxy7:3128";
else if (shExpMatch(url, "*training.company.com*")) return "PROXY companyproxy7:3128";
else if (shExpMatch(url, "*farm.company.com*")) return "PROXY companyproxy7:3128";
else if (shExpMatch(host, "*.fa.company.com")) return "PROXY companyproxy7:3128";
else if (shExpMatch(url, "https://servicedesk.company.com/*")) return "PROXY companyproxy7:3128";
else if (shExpMatch(url, "https://servicedesk.company.com:8080/*")) return "PROXY companyproxy7:3128";
…

И в конце файла мы видим что-то вроде (и даже с комментарием!):


/* Default Traffic Forwarding */
return "PROXY companyproxy7:3128";

Помимо проблем, что мы уже рассмотрели ранее, блок кода выше полностью бесполезен и лишь замедляет работу браузера, ведь в конечном итоге по умолчанию используется тот же самый proxy-сервер.


Пример восьмой


Проблема номер восемь связана с внутренними ресурсам компании.
Скажем, есть у нас такой код:


if (host == 'internal') return 'DIRECT';
if (host == 'mail') return 'DIRECT';
if (host == 'files') return 'DIRECT';

То есть PAC-файл проверяет, не обратился ли браузер ко внутреннему ресурсу по hostname без указания домена, так как домены поиска заданы на PC. Хорошим решением для оптимизации таких проверок будет использование всего одной функции:


 if (isPlainHostName(host)) {
return 'DIRECT';

Пример девятый


Задача оптимизации номер 9 близка к предыдущей и опять же появляется при использовании внутренних ресурсов организации. Что делать, если пользователь может запросить один и тот же сайт как с указанием домена, так и без него, как на примере ниже?


if (host == 'mail') return 'DIRECT';
if (host == 'mail.resource.lan’) return 'DIRECT';
if (host == 'files') return 'DIRECT';
if (host == 'files.resource.lan') return 'DIRECT';

Оптимизировать такие проверки можно довольно просто, если задействовать функцию localHostOrDomainIs:


if (localHostOrDomainIs(host, "mail.resource.lan") return 'DIRECT';
if (localHostOrDomainIs(host, "files.resource.lan”) return 'DIRECT';

Пример десятый


Спорная проблема оптимизации if условий. Суть такова, что в PAC-файлах после проверки какого-либо условия по if в подавляющем большинстве случаев идёт возврат из функции FindProxyForURL по return, так зачем же тогда использовать конструкцию, приведенную ниже?


if
..
else if
..
else

Результаты тестирования FF подсказывают, что оптимальным будет использование OR условия ( || ):


image


Для Chrome результат будет несколько иным, но блок if / else if опять проигрывает:


image


Повторить тест для своего окружения читатель может здесь: https://jsperf.com/shexpmatch-vs-host-string-vs-dnsdomainis


На основе сказанного делаем вывод, что оптимальнее будет использовать if statement или if + or, хотя тестирование IE11 даёт кардинально противоположный результат. Возможно, хорошей идеей было бы задействовать switch statement, но данный опус я уже дописываю глубоко ночью, а потому отдаю тестирование этого кейса на откуп многоуважаемому читателю.


Вместо заключения


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


Полезные ссылки


Cisco Systems о PAC-файлах
Полезный сайт с описанием функций, используемых в PAC-файлах
Сайт, предоставляющий удобный интерфейс для тестирования производительности JavaScript-кода
Здесь можно проверить логику работы PAC-файла
Ещё один ресурс, на котором можно отлаживать JavaScript-код, включая PAC-файлы

Проголосовать:
+6
Сохранить: