Limită
Pe LinkedIn există o astfel de limitare — . Este foarte probabil că tu, la fel ca și mine până nu demult, nu te-ai confruntat și nu ai auzit despre ea.

Esenta limitei este că, dacă folosești căutarea de persoane în afara contactelor tale prea des (nu există metrici exacte, algoritmul decide în funcție de acțiunile tale — cât de des și mult ai căutat, ai adăugat persoane), atunci rezultatul căutării va fi limitat la trei profiluri, în loc de 1000 (implicit 100 de pagini, cu 10 profiluri pe pagină). Limita se resetează la începutul fiecărei luni. Evident, .
Dar, nu cu mult timp în urmă, pentru un proiect personal, am început să experimentez mult cu căutarea pe LinkedIn și, dintr-o dată, am primit această limitare. Evident, nu mi-a plăcut prea mult, deoarece nu o foloseam în scopuri comerciale, așa că prima idee a fost să studiez limitarea și să încerc să o ocolesc.
[O precizare importantă — materialele din articol sunt prezentate exclusiv în scopuri informative și educaționale. Autorul nu încurajează utilizarea lor în scopuri comerciale.]
Studiază problema
Avem: în loc de zece profiluri cu paginare, căutarea oferă doar trei, după care apare un bloc cu „recomandarea” contului premium, iar mai jos se află profiluri estompate și non-clickabile.
Imediat, mâna se duce spre consola de dezvoltare pentru a verifica aceste profiluri ascunse — poate putem elimina anumite stiluri care aplică estomparea sau extrage informații din blocul din marcaj. Dar, așa cum era de așteptat, aceste profiluri sunt doar și nu conțin nicio informație.

Bine, acum să ne uităm la tab-ul Network și să verificăm dacă într-adevăr se activează o alternativă pentru rezultatele căutării, care returnează doar trei profiluri. Găsim cererea de care avem nevoie către “/api/search/blended” și ne uităm la răspuns.

Profilurile vin într-un array `included`, dar există 15 entități în el. În acest caz, primele trei dintre ele sunt obiecte cu informații suplimentare, fiecare obiect conține informații despre un profil specific (de exemplu, dacă profilul este premium).

Următoarele 12 sunt profiluri reale - rezultate ale căutării, dintre care ne vor fi arătate doar trei. Așa cum se poate deduce, sunt afișați doar cei pentru care vine informație suplimentară (primele trei obiecte). De exemplu, dacă luăm un răspuns de la un profil fără limită, vor veni 28 de entități - 10 obiecte cu informații suplimentare și 18 profiluri.
Răspuns pentru profil fără limită

De ce vin mai multe profiluri decât cele 10 solicitate, având în vedere că sunt solicitate exact 10, și acestea nu participă deloc la afișare, chiar și pe pagina următoare nu vor fi - încă nu știu. Dacă analizăm URL-ul solicitării, putem observa că count=10 (câte profiluri să returnăm în răspuns, maxim 49).
Aș fi bucuros de orice comentarii în acest sens.
Experimentăm
Bine, cel mai important, acum știm cu siguranță - profilurile vin în răspuns mai multe decât ni se arată. Asta înseamnă că putem obține mai multe date, chiar și cu limita. Haideți să încercăm să accesăm API-ul singuri, direct din consolă, folosind fetch.

Așa cum era de așteptat, primim o eroare, 403. Aceasta este legată de securitate, aici nu trimitem token-ul CSRF (. Pe scurt - fiecărui apel i se adaugă un token unic, care este verificat pe server pentru autenticitate).

Acesta poate fi copiat din orice alt apel de succes sau din cookies, unde este stocat în câmpul 'JSESSIONID'.
Unde găsim token-ulAntetul altui apel:

Sau din cookie-uri, direct prin consolă:

Încercăm din nou, de data aceasta transmitem în fetch setările, în care specificăm ca parametru în antet token-ul nostru csrf.

Succes, primim toate cele 10 profiluri. :tada:
Datorită diferenței de antete, structura răspunsului se diferențiază puțin de ceea ce vine în apelul original. Putem obține aceeași structură dacă adăugăm 'Accept: 'application/vnd.linkedin.normalized+json+2.1' în obiectul nostru, alături de token-ul csrf.
Exemplu de răspuns cu antetul adăugat
Ce urmează?
În continuare, se poate modifica (manual sau automatiza) parametrul `start`, care indică indexul de la care ni se vor returna 10 profiluri (implicit = 0) din întregul rezultat al căutării. Cu alte cuvinte, incrementându-l cu 10 după fiecare apel, obținem o afișare paginată normală, câte 10 profiluri de fiecare dată.
În această etapă, aveam suficiente date și libertate pentru a continua lucrul la proiectul meu personal. Dar ar fi fost păcat să nu încerc să afișez aceste date direct la fața locului, avându-le deja. Nu ne vom baga în Ember, care este folosit pe frontend. Pe site era integrat jQuery, și, amintindu-mi cunoștințele de bază ale sintaxei, pot crea următorul cod în câteva minute.
Cod 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="/ro/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="/ro/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>`
);
};
// apelăm API-ul, obținem datele și randăm profilurile
const fetchProfiles = () => {
// token
const csrf = 'ajax:9082932176494192209';
// obiect cu setările cererii, transmitem tokenul
const settings = { headers: { 'csrf-token': csrf } }
// url-ul cererii, cu un index de start dinamic la final
const url = `https://www.linkedin.com/voyager/api/search/blended?count=10&filters=List(geoRegion-jp0,network-S,resultType-PEOPLE)&origin=FACETED_SEARCH&q=all&queryContext=List(spellCorrectionEnabled-true,relatedSearchesEnabled-true)&start=${nextItemIndex}`;
/* facem cererea, pentru fiecare profil din răspuns apelăm randarea blocului, și după incrementăm indexul de start cu 10 */
fetch(url, settings).then(response => response.json()).then(data => {
data.elements[0].elements.forEach(createProfileBlock);
nextItemIndex += 10;
});
};
// ștergem toate profilurile din listă
$('.search-results__list').find('li').remove();
// inserăm butonul de încărcare a profilurilor
$('.search-results__list').after('<button id="load-more">Încărcați mai multe</button>');
// adăugăm funcționalitatea pe buton
$('#load-more').addClass('artdeco-button').on('click', fetchProfiles);
// setăm indexul profilului pentru cerere
window.nextItemIndex = 0;
Dacă executăm acest cod direct în consola paginii de căutare, va adăuga un buton care va încărca 10 noi profiluri la fiecare apăsare, afișându-le într-o listă. Desigur, trebuie să schimbăm tokenul și URL-ul conform necesităților. Blocul de profil va conține numele, funcția, locația, linkul profilului și o imagine de placeholder.

Concluzie
Astfel, cu un minim de efort, am reușit să găsim o vulnerabilitate și să revenim la căutarea fără restricții. A fost suficient să analizăm datele și drumul acestora, aruncând o privire în cererea în sine.
Nu pot spune că aceasta reprezintă o problemă serioasă pentru LinkedIn, deoarece nu prezintă nicio amenințare. Cel mult, este o pierdere de profit din cauza unor „învăluiri” care permit să nu plătești pentru premium. Poate că un astfel de răspuns al serverului este necesar pentru funcționarea corectă a altor părți ale site-ului, sau este pur și simplu o lene a dezvoltatorilor, o lipsă de resurse care să permită să facă lucrurile bine. (Restricția a apărut în ianuarie 2015, înainte de aceasta nu existau limite).
P.S.
Desigur, codul jQuery este un exemplu destul de primitiv al capacităților sale. În momentul de față am creat o extensie pentru browser pentru nevoile mele. Aceasta adaugă butoane de control și redă profiluri complete cu imagini, un buton de invitație și conexiuni comune. În plus, colectează dinamic filtre pentru locații, companii și altele, extrăgând tokenul din cookie. Așa că nu mai este nevoie să codificăm nimic de mână. De asemenea, adaugă câmpuri suplimentare de configurare, cum ar fi „câte profiluri să se solicite simultan, până la 49”.

Încă lucrez la această extensie și plănuiesc să o fac disponibilă publicului. Scrieți-mi dacă vă interesează.
Sursa: habr.com


