Обходим лимит поиска LinkedIn, играя с API

Лимит


Есть на LinkedIn такое ограничение — Лимит коммерческого использования. Крайне вероятно, что вы, как и я до недавнего времени, никогда не сталкивались и не слышали о нем.



Суть лимита в том, что если вы используете поиск людей вне ваших контактов слишком часто (точных метрик нет, решает алгоритм, на основе ваших действий — как часто и много искали, добавляли людей), то результат поиска будет ограничен тремя профилями, вместо 1000 (по умолчанию 100 страниц, по 10 профилей на страницу). Лимит сбрасывается в начале каждого месяца. Естественно, премиум аккаунты такого ограничения не имеют.

Но не так давно, для одного пет-проекта, я начал много играться с поиском на LinkedIn и внезапно получил это ограничение. Естественно, такое мне не очень понравилось, ведь я не использовал его в каких-либо коммерческих целях, поэтому первой мыслью было изучить ограничение и попытаться его обойти.

[Важное уточнение — материалы в статье представлены исключительно в ознакомительных и обучающих целях. Автор не поощряет их использование в коммерческих целях.]

Изучаем проблему


Имеем: вместо десяти профилей с пагинацией, поиск выдает только три, после которых вставляется блок с “рекомендацией” премиум аккаунта и ниже идут размытые и не кликабельные профили.

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



Хорошо, теперь посмотрим во вкладку Network и проверим, действительно ли срабатывает альтернативная выдача результатов поиска, возвращающая только три профиля. Находим интересующий нас запрос к “/api/search/blended” и смотрим на ответ.



Профили приходят в массиве `included`, но сущностей в нем аж 15. В данном случае, первые три из них — объекты с дополнительной информацией, каждый объект содержит информацию по конкретному профилю (например, является ли профиль премиумом).



Последующие 12 это реальные профили — результаты поиска, из которых нам покажут только три. Как уже можно догадаться, показывает только тех, на кого приходит дополнительная информация (первые три объекта). Например, если взять ответ с профиля без лимита, то придет 28 сущностей — 10 объектов с доп. информацией и 18 профилей.

Ответ для профиля без лимита



Почему профилей приходит больше 10, хотя запрашивается именно 10, и они никак не участвуют в отображении, даже на следующей странице их не будет — пока не знаю. Если проанализировать урл запроса то можно увидеть, что count=10 (сколько профилей вернуть в ответе, максимум 49).



Буду рад любым коментариям по этому поводу.

Экспериментируем


Хорошо, самое главное мы теперь точно знаем — профилей приходит в ответе больше, чем нам показывают. Значит мы можем достать больше данных, не смотря на лимит. Давайте попробуем дернуть апи сами, прямо из консоли, при помощи fetch.



Ожидаемо, получаем ошибку, 403. Это связано с безопасностью, здесь мы не отсылаем CSRF токен (CSRF на Википедии. Если в двух словах — к каждому запросу добавляется уникальный токен, который проверяется на сервере на подлинность).



Его можно скопировать из любого другого успешного запроса или же из cookies, где он хранится в поле ‘JSESSIONID’.

Где найти токен
Заголовок другого запросa:



Или из куки, прямо через консоль:



Пробуем еще раз, в этот раз передаем в fetch настройки, в которых указываем параметром в header наш csrf-token.



Успех, нам приходят все 10 профилей. :tada:

Из-за разницы заголовков структура ответа немного отличается от того, что в приходит в оригинальном запросe. Можно получить такую же структуру, если добавить 'Accept: 'application/vnd.linkedin.normalized+json+2.1', к нам в объект, рядом с csrf токеном.
Пример ответа с добавленным заголовком


Больше о заголовке Accept

Что дальше?


Дальше можно редактировать (руками или автоматизировать) параметр `start`, указывающий на индекс, начиная с которого нам отдадут 10 профилей (по-умолчанию = 0) из всего результата поиска. Иначе говоря, инкрементируя его на 10 после каждого запроса, у нас получается обычная постраничная выдача, по 10 профилей за раз.

На этом этапе у меня было достаточно данных и свободы, чтобы продолжать работу над пет-проектом. Но грех было не попробовать эти данные отобразить прямо на месте, раз уж они на руках. В Ember, который используется на фронте, лезть не будем. На сайте был подключен jQuery, и откопав в памяти знания базового синтаксиса, можно за пару минут создать следующее.

Код на jQuery
/* рендер блока, принимаем данные профиля и вставляем блок в список профилей используя эти данные */
const  createProfileBlock = ({ headline, publicIdentifier, subline, title }) => {
    $('.search-results__list').append(
        `<li class="search-result search-result__occluded-item ember-view">
            <div class="search-entity search-result search-result--person search-result--occlusion-enabled ember-view">
                <div class="search-result__wrapper">
                    <div class="search-result__image-wrapper">
                        <a class="search-result__result-link ember-view" href="/in/${publicIdentifier}/">
                            <figure class="search-result__image">
                                <div class="ivm-image-view-model ember-view">
                                    <img class="lazy-image ivm-view-attr__img--centered EntityPhoto-circle-4  presence-entity__image EntityPhoto-circle-4 loaded" src="http://www.userlogos.org/files/logos/give/Habrahabr3.png" />
                                </div>
                            </figure>
                        </a>
                    </div>
                    
                    <div class="search-result__info pt3 pb4 ph0">
                        <a class="search-result__result-link ember-view" href="/in/${publicIdentifier}/">
                            <h3 class="actor-name-with-distance search-result__title single-line-truncate ember-view">
                                ${title.text}
                            </h3>
                        </a>

                        <p class="subline-level-1 t-14 t-black t-normal search-result__truncate">${headline.text}</p>

                        <p class="subline-level-2 t-12 t-black--light t-normal search-result__truncate">${subline.text}</p>
                    </div>
                </div>
            </div>
        <li>`
    );
};

// дергаем апи, получаем данные и рендерим профили
const fetchProfiles = () => {
    // токен
   const csrf = 'ajax:9082932176494192209';
    
   // объект с настройками запроса, передаем токен
   const settings = { headers: { 'csrf-token': csrf } }

    // урл запроса, с динамическим индексом старта в конце
   const url = `https://www.linkedin.com/voyager/api/search/blended?count=10&filters=List(geoRegion-%3Ejp%3A0,network-%3ES,resultType-%3EPEOPLE)&origin=FACETED_SEARCH&q=all&queryContext=List(spellCorrectionEnabled-%3Etrue,relatedSearchesEnabled-%3Etrue)&start=${nextItemIndex}`; 
    /* делаем запрос, для каждого профиля в ответе вызываем рендер блока, и после инкрементируем стартовый индекс на 10 */
    fetch(url, settings).then(response => response.json()).then(data => {
        data.elements[0].elements.forEach(createProfileBlock);
        nextItemIndex += 10;
});
};


// удаляем все профили из списка
$('.search-results__list').find('li').remove();
// вставляем кнопку загрузки профилей
$('.search-results__list').after('<button id="load-more">Load More</button>');
// добавляем функционал на кнопку
$('#load-more').addClass('artdeco-button').on('click', fetchProfiles);

// ставим по умолчания индекс профиля для запроса
window.nextItemIndex = 0;


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



Заключение


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

Я не могу сказать что это является серьезной проблемой для LinkedIn, потому что никакой угрозы не несет. Максимум, это потерянная прибыль из-за подобных «обходов», позволяющая не платить за премиум. Возможно, такой ответ сервера необходим для корректной работы других частей сайта, или же это просто лень разработчиков недостаток ресурсов, не позволяющий сделать хорошо. (Ограничение появилось с января 2015 года, до этого лимита не было).

P.S.


Старый P.S.
Ествественно, код на jQuery довольно примитивный пример возможностей. В данный момент я создал extension для браузера под свои нужды. Он добавляет кнопки контроля и рендерит полноценные профили с картинками, кнопкой приглашения и общими коннектами. Плюс динамически собирает фильтры локаций, компаний и прочего, достает токен из куки. Так что ничего хардокдить уже не нужно. Ну и добавляет дополнительные поля настроек, а-ля «сколько профилей запрашивать за раз, до 49».



Над этим дополнением я все еще работаю и в планах выложить его в открытый доступ. Пишите если вам интересно.


По многочисленным просьбам выложить дополнение в открытый доступ, я создал дополнение для браузера и выложил его для общего использования (бесплатно и даже без майнеров). Там реализован не только функционал обхода лимита, но и прочие удобности. Ознакомиться и скачать можно здесь — adam4leos.github.io

Так как это альфа версия, не стеснятесь писать мне о багах, идеях и даже стремном шикарном UI. Я продолжаю развивать дополнение и буду периодически выкладывать новые версии.
Share post

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 13

    0
    Что мешало им фильтровать данные на сервере не понятно
      +1
      лень разработчиков недостаток ресурсов?
        0
        А нужно ли им это было?
        Когда платный функционал реализуется по принципу «если нельзя, но очень хочется, то можно»© это тоже имеет свои плюсы. Хочешь заморачиваться — обходи, не хочешь заморачиваться — плати.
        Но если плагин пойдет в массы — стопудово переделают так или иначе.
          0
          Не совсем так.
          «Хочешь заморачиваться — обходи», но потом, когда все привыкнут, сделают серверную фильтрацию и придется покупать.
          0
          зачем? целевой рынок обозначен, заморачиватся ежедневно в использование подобных «лайв хаков» врядли будет опасно для бизнеса, зато затрата x-часов на создание контроля подачи «воздуха» внутреними разрабами может быть опасным или как минимум очень затратным действием, которое врядли повлияет на годовой отчёт по сбыту. А вот воздух продать — это ведёт за собой повышение ;)
          0
          Автор сделал более или менее не плохой анализ и нашел решение проблеме.
          Однако, интересует насколько интересно это публике, ибо проблемы такого рода встречаются почти ежедневно, решение некоторых тривиальны, другие заставляют помучатся день-два. Благодаря инструментам chrome и js решение этих проблем, как мне кажется, более или менее тривиально для среднего хабравчана.
          Это ни в коем случае не критика к автору, а лишь попытка разобраться в потребности материала, который я мог бы предоставить. Спасибо
            +1
            Автор, после проделанной работы тебя просто обязаны пригласить работать в LinkedIn! Хочу такой плагин!
              0
              Спасибо, скоро будет в открытом доступе.
                0
                Уж осень близится, а Германа всё нет(ц) ;-)
                Ждем
              +1
              Автор, спасибо за расследование. Так уж получилось, что я работаю в Linkedin. Переслал эту статью людям ответственным за поиск и они провели расследование.
              Вот их ответ: «we are not leaking blocked results. what the blog post author thinks are additional search results are actually the profile data for the existing results' social proof».
              Например, на первой картинке это Darya Shulha, как я понимаю.
                +2
                Огромное спасибо за ответ и, в частности, за то, что передали это ответственным за поиск. Очень интересно. Только что-то не сходится:

                1) Как вы верно определили, social proof — это профили из общих контактов. Их показывает перед кол-вом общих контактов, и приходит их до трех на каждый профиль выдачи.
                Пример
                up-to-three-social-proofs

                Но дело в том, что эти socialproof не являются самостоятельными профилями в выдаче, а лишь дополнительные данные одного из полей у каждого профиля из результата поиска.
                Что в объектах socialproof
                what-is-inside-social-proof


                2) Так как уже новый месяц, значит лимит у меня обнулили. Теперь мы можем сравнить результат поиска сейчас, без лимита, с результатом поиска из статьи, когда был лимит и я его обходил. Если верить ответу ребят из LinkedIn, то совпасть может до трех человек, т.к. тогда у меня был лимит и остальные люди там из socialproof.
                Но это не так
                Note: пускай вас не смущает небольшое расхождение, LinkedIn постоянно ранжирует выдачу в зависимости от ваших действий (можете убедиться самостоятельно: совершаете поиск и запоминаете его результат, затем делаете несколько поисков по разным странам и повторяте первый)

                compare-limited-result-with-unlimited


                3) Так как все скриншоты в статье сделаны во время одной сессии, те же самые выводы можно сделать и по статье, ведь если предположить, что с лимитом я получал людей из socialproof, то это не так по ряду причин:

                * имена людей в socialproff (первый скриншот) не совпадали с теми, кто у меня был в выдаче (последний скриншот);
                * люди в выдаче не были моими контактами, потому что была активна кнопка connect (да, в расширении я добавил ей полноценный функционал), а в socialproof — лишь общие знакомые
                да, если их нет, в поле пустой массив
                empty-social-proof


                Почему разработчики так вам ответили, я кажется догадываюсь. Дело в том, что в новом формате (как упоминалось в статье, это запрос с заголовком 'Accept: 'application/vnd.linkedin.normalized+json+2.1'), действительно приходит целая пачка профилей, и среди них действительно есть профили из socialproof.
                Пример такого ответа
                old-format-response

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

                  0
                  Сейчас донесут и прикроют лазейку. Вы уж лучше plugin выпускайте, очень нужен. Linkdin самый эффективный способ поиска работы в США. Удачи!
                0
                Прошу прощения за задержку, но так как запросов на дополнение было очень много, то пришлось делать качественно, а не просто добавить пару кнопок и сотню багов.

                Альфа версия готова, последний раздел статьи обновлен.
                Ознакомится и скачать дополнение можно здесь adam4leos.github.io (бесплатно и даже без майнеров)

                Так же буду рад любым предложениям/критике/багрепортам, так как работу над дополнением продолжаю и буду выкладывать новые версии.

                cc KMU win32nipuh shuvaevgl

                Only users with full accounts can post comments. Log in, please.