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

    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-файлы

    ICL Services 56,38
    Компания
    Поделиться публикацией
    Комментарии 9
    • 0
      Скажите, а вы уверены, что в третьем примере ошибка дублирования условий, а не ошибка порядка строк?
      Пример больше подходит для ситуации когда задан неверный порядок обхода условий, хотя и описываемая ошибка естественно присутствует.
      • 0

        Добрый день. Да, если возвращается одна и та же proxy (или как в нашем случае, DIRECT). Прошу заметить ещё раз, что тут есть ещё более грубая ошибка с проверкой по host, а не resolved address.

        • 0
          Вывод: как ни пиши — все равно не уйдешь за пределы погрешности и без того мизерной величины.
          • 0
            Спасибо за комментарий, но я с Вами не могу согласиться. Вопрос не только в производительности, но в первую очередь в правильности применения функций, обратите на это внимание, пожалуйста.
          • 0
            Зачем у вас во многих примерах else if? Вы же всё равно используете return.

            В таблицах тестов вы ничего не напутали? Сначала вы утверждаете, что shExpMatch — очень медленная и вместо неё лучше host ==, а из таблиц видно, что для хрома разница несущественна, а для Firefox — shExpMatch работает значительно быстрее.

            Ну и вообще, тесты странные. Ваш «оптимальный» вариант возвращает один единственный прокси, а остальные варианты выбирают один из трёх
            • 0
              Спасибо за Ваш комментарий, давайте по-порядку:

              >>Зачем у вас во многих примерах else if? Вы же всё равно используете return.
              Примеры взяты из реальных кейсов внедрения, так что вопрос очень правильный и я его тоже задавал коллегам: зачем использовать else if, если мы в любом случае возвращаем значение по return? Смотрите пример десятый, пожалуйста.

              >>В таблицах тестов вы ничего не напутали?
              Нет, все абсолютно верно. И именно потому см. Disclaimer, повторю, тесты лучше провести повторно, самостоятельно, потому как они могут быть спорные для разных случаев использования. Я крайне рекомендую воспользоваться преимуществами jsperf и при наличие аккаунта на github повторить тесты или даже модифицировать их, убедиться в преимуществах тех или иных функций самостоятельно.

              >>Сначала вы утверждаете, что shExpMatch — очень медленная
              К сожалению, совсем не могу согласиться с этим комментарием. «очень медленная» — это слишком ультимативное высказывание, я такого не писал, смотрите внимательнее, пожалуйста, пример первый. Речь идёт именно о первой строке кода с "*" и разницы в 1% в моём случае. И ещё раз, проблема даже не в оптимальности функции, а в уместности её применения, что значительно важнее.

              >>Ваш «оптимальный» вариант возвращает один единственный прокси, а остальные варианты выбирают один из трёх
              Если речь идёт о примере десятом, то опять же, оптимальной будет dnsDomainIs, который возвращает один из трех, а не единственный.
              • 0
                > Если речь идёт о примере десятом, то опять же, оптимальной будет dnsDomainIs, который возвращает один из трех, а не единственный
                Вы же сами пишете «оптимальным будет использование OR условия»
                • 0
                  В общем случае да, dnsDomainIs более оптимален. Но если говорить конкретно про замену конструкции if/else if/else в десятом примере, то не всегда можно использовать более оптимальную функцию dnsDomainIs. Если требуется оптимизировать условия, то для FF в моём случае OR будет оптимальнее чем else if. Скриншот же приведён для всех вариантов, видимо он и смущает Вас.
                  • 0
                    Ну и добавлю, для полного понимания, если Вы не можете использовать OR (т.к. результат только один return) и нет возможности задействовать dnsDomainIs, я бы задействовал блок с именем «host» для обоих браузеров.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое