สวัสดี! สองสามวันที่ฉันเขียนเกี่ยวกับ โปรแกรมส่วนตัวเล็กๆ และบอกว่าการใช้ API ที่ไม่มีเอกสาร “ลับ” เป็นเรื่องสนุก ซึ่งคุณต้องคัดลอกคุกกี้ออกจากเบราว์เซอร์เพื่อเข้าถึง
มีคนถามวิธีทำสิ่งนี้ ดังนั้นฉันจึงต้องการอธิบายว่าอย่างไร เพราะมันค่อนข้างตรงไปตรงมา นอกจากนี้ เราจะพูดคุยกันเล็กน้อยเกี่ยวกับสิ่งที่อาจผิดพลาด ปัญหาด้านจริยธรรม และวิธีที่สิ่งนี้ใช้กับ API ที่ไม่มีเอกสารของคุณ
ตัวอย่างเช่น ลองใช้ Google แฮงเอาท์ ฉันเลือกสิ่งนี้ไม่ใช่เพราะมันเป็นตัวอย่างที่มีประโยชน์ที่สุด (ฉันคิดว่ามี API อย่างเป็นทางการซึ่งน่าจะใช้งานได้จริงมากกว่า) แต่เนื่องจากไซต์จำนวนมากที่มีประโยชน์จริง ๆ คือไซต์ขนาดเล็กกว่าที่เสี่ยงต่อการละเมิดมากกว่า ดังนั้นเราจะใช้ Google แฮงเอาท์เพราะฉันแน่ใจ 100% ว่าแบ็กเอนด์ของ Google แฮงเอาท์ได้รับการออกแบบให้มีความยืดหยุ่นในการพูดคุยในลักษณะนี้
มาเริ่มกันเลย!
ขั้นตอนที่ 1: ค้นหาเครื่องมือสำหรับนักพัฒนาเพื่อหาคำตอบ JSON ที่มีแนวโน้ม
ฉันเริ่มต้นด้วยการไปที่ https://hangouts.google.com เปิดแท็บเครือข่ายในเครื่องมือสำหรับนักพัฒนา Firefox และค้นหาคำตอบ JSON คุณสามารถใช้เครื่องมือสำหรับนักพัฒนา Chrome ได้เช่นกัน
หน้าตาประมาณนี้
คำขอเป็นตัวเลือกที่ดีหากมีคำว่า “json” ในคอลัมน์ “ประเภท”
ฉันต้องมองไปรอบๆ สักพักจนกระทั่งพบสิ่งที่น่าสนใจ แต่ในที่สุดฉันก็พบจุดปลาย “ผู้คน” ที่ดูเหมือนจะส่งคืนข้อมูลเกี่ยวกับผู้ติดต่อของฉัน ฟังดูน่าสนุก เรามาลองดูกัน
ขั้นตอนที่ 2: คัดลอกเป็น cURL
ต่อไป ฉันคลิกขวาที่คำขอที่ฉันสนใจ แล้วคลิก “คัดลอก” -> “คัดลอกเป็น cURL”
จากนั้นฉันวางคำสั่ง curl
ในเทอร์มินัลแล้วเรียกใช้ นี่คือสิ่งที่เกิดขึ้น
$ curl 'https://people-pa.clients6.google.com/v2/people/?key=REDACTED' -X POST ........ (a bunch of headers removed) Warning: Binary output can mess up your terminal. Use "--output -" to tell Warning: curl to output it to your terminal anyway, or consider "--output Warning: <FILE>" to save to a file.
คุณอาจจะกำลังคิด – ที่แปลก ข้อผิดพลาด “เอาต์พุตไบนารีนี้อาจทำให้เทอร์มินัลของคุณยุ่งเหยิง” คืออะไร นั่นเป็นเพราะโดยค่าเริ่มต้น เบราว์เซอร์จะส่ง Accept-Encoding: gzip, deflate
header ไปยังเซิร์ฟเวอร์ เพื่อรับเอาต์พุตที่บีบอัด
เราสามารถแตกไฟล์ได้โดยการไพพ์เอาต์พุตไปที่ gunzip
แต่ฉันคิดว่ามันง่ายกว่าที่จะไม่ส่งส่วนหัวนั้น เรามาลบส่วนหัวที่ไม่เกี่ยวข้องกัน
ขั้นตอนที่ 3: ลบส่วนหัวที่ไม่เกี่ยวข้อง
นี่คือบรรทัดคำสั่ง curl
แบบเต็มที่ฉันได้รับจากเบราว์เซอร์ มีมากที่นี่! ฉันเริ่มต้นด้วยการแยกคำขอด้วยแบ็กสแลช ( \
) เพื่อให้แต่ละส่วนหัวอยู่ในบรรทัดที่แตกต่างกันเพื่อให้ทำงานได้ง่ายขึ้น:
curl 'https://people-pa.clients6.google.com/v2/people/?key=REDACTED' \ -X POST \ -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:96.0) Gecko/20100101 Firefox/96.0' \ -H 'Accept: */*' \ -H 'Accept-Language: en' \ -H 'Accept-Encoding: gzip, deflate' \ -H 'X-HTTP-Method-Override: GET' \ -H 'Authorization: SAPISIDHASH REDACTED' \ -H 'Cookie: REDACTED' -H 'Content-Type: application/x-www-form-urlencoded' \ -H 'X-Goog-AuthUser: 0' \ -H 'Origin: https://hangouts.google.com' \ -H 'Connection: keep-alive' \ -H 'Referer: https://hangouts.google.com/' \ -H 'Sec-Fetch-Dest: empty' \ -H 'Sec-Fetch-Mode: cors' \ -H 'Sec-Fetch-Site: same-site' \ -H 'Sec-GPC: 1' \ -H 'DNT: 1' \ -H 'Pragma: no-cache' \ -H 'Cache-Control: no-cache' \ -H 'TE: trailers' \ --data-raw 'personId=101777723309&personId=1175339043204&personId=1115266537043&personId=116731406166&extensionSet.extensionNames=HANGOUTS_ADDITIONAL_DATA&extensionSet.extensionNames=HANGOUTS_OFF_NETWORK_GAIA_GET&extensionSet.extensionNames=HANGOUTS_PHONE_DATA&includedProfileStates=ADMIN_BLOCKED&includedProfileStates=DELETED&includedProfileStates=PRIVATE_PROFILE&mergedPersonSourceOptions.includeAffinity=CHAT_AUTOCOMPLETE&coreIdParams.useRealtimeNotificationExpandedAcls=true&requestMask.includeField.paths=person.email&requestMask.includeField.paths=person.gender&requestMask.includeField.paths=person.in_app_reachability&requestMask.includeField.paths=person.metadata&requestMask.includeField.paths=person.name&requestMask.includeField.paths=person.phone&requestMask.includeField.paths=person.photo&requestMask.includeField.paths=person.read_only_profile_info&requestMask.includeField.paths=person.organization&requestMask.includeField.paths=person.location&requestMask.includeField.paths=person.cover_photo&requestMask.includeContainer=PROFILE&requestMask.includeContainer=DOMAIN_PROFILE&requestMask.includeContainer=CONTACT&key=REDACTED'
นี้อาจดูเหมือนจำนวนมากของสิ่งต่างๆ ในตอนแรก แต่คุณไม่จำเป็นต้องคิดว่ามันหมายถึงอะไรในขั้นตอนนี้ คุณเพียงแค่ต้องลบบรรทัดที่ไม่เกี่ยวข้อง
ฉันมักจะคิดออกว่าส่วนหัวใดที่ฉันสามารถลบได้ด้วยการลองผิดลองถูก – ฉันจะลบส่วนหัวออกไปเรื่อยๆ จนกว่าคำขอจะเริ่มล้มเหลว โดยทั่วไป คุณอาจไม่ต้องการ Accept*
, Referer
, Sec-*
, DNT
, User-Agent
และแคชส่วนหัว
ในตัวอย่างนี้ ฉันสามารถตัดคำขอเป็นดังนี้:
curl 'https://people-pa.clients6.google.com/v2/people/?key=REDACTED' \ -X POST \ -H 'Authorization: SAPISIDHASH REDACTED' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -H 'Origin: https://hangouts.google.com' \ -H 'Cookie: REDACTED'\ --data-raw 'personId=101777723309&personId=1175339043204&personId=1115266537043&personId=116731406166&extensionSet.extensionNames=HANGOUTS_ADDITIONAL_DATA&extensionSet.extensionNames=HANGOUTS_OFF_NETWORK_GAIA_GET&extensionSet.extensionNames=HANGOUTS_PHONE_DATA&includedProfileStates=ADMIN_BLOCKED&includedProfileStates=DELETED&includedProfileStates=PRIVATE_PROFILE&mergedPersonSourceOptions.includeAffinity=CHAT_AUTOCOMPLETE&coreIdParams.useRealtimeNotificationExpandedAcls=true&requestMask.includeField.paths=person.email&requestMask.includeField.paths=person.gender&requestMask.includeField.paths=person.in_app_reachability&requestMask.includeField.paths=person.metadata&requestMask.includeField.paths=person.name&requestMask.includeField.paths=person.phone&requestMask.includeField.paths=person.photo&requestMask.includeField.paths=person.read_only_profile_info&requestMask.includeField.paths=person.organization&requestMask.includeField.paths=person.location&requestMask.includeField.paths=person.cover_photo&requestMask.includeContainer=PROFILE&requestMask.includeContainer=DOMAIN_PROFILE&requestMask.includeContainer=CONTACT&key=REDACTED'
ดังนั้นฉันต้องการแค่ 4 ส่วนหัว: Authorization
, Content-Type
, Origin
และ Cookie
ที่สามารถจัดการได้มากขึ้น
ขั้นตอนที่ 4: แปลเป็น Python
ตอนนี้เรารู้แล้วว่าเราต้องการ header อะไร เราก็สามารถแปลคำสั่ง curl
เป็นโปรแกรม Python ได้! ส่วนนี้ยังเป็นกระบวนการทางกลที่ค่อนข้างสวย เป้าหมายคือการส่งข้อมูลเดียวกันกับ Python เหมือนกับที่เราเคยทำกับ curl
นี่คือสิ่งที่ดูเหมือน สิ่งนี้เหมือนกับคำสั่ง curl
ก่อนหน้าทุกประการ แต่ใช้ requests
ของ Python ฉันยังแยกสตริงเนื้อหาคำขอที่ยาวมากออกเป็นอาร์เรย์ของทูเพิลเพื่อให้ทำงานด้วยการเขียนโปรแกรมได้ง่ายขึ้น
import requests import urllib data = [ ('personId','101777723'), # I redacted these IDs a bit too ('personId','117533904'), ('personId','111526653'), ('personId','116731406'), ('extensionSet.extensionNames','HANGOUTS_ADDITIONAL_DATA'), ('extensionSet.extensionNames','HANGOUTS_OFF_NETWORK_GAIA_GET'), ('extensionSet.extensionNames','HANGOUTS_PHONE_DATA'), ('includedProfileStates','ADMIN_BLOCKED'), ('includedProfileStates','DELETED'), ('includedProfileStates','PRIVATE_PROFILE'), ('mergedPersonSourceOptions.includeAffinity','CHAT_AUTOCOMPLETE'), ('coreIdParams.useRealtimeNotificationExpandedAcls','true'), ('requestMask.includeField.paths','person.email'), ('requestMask.includeField.paths','person.gender'), ('requestMask.includeField.paths','person.in_app_reachability'), ('requestMask.includeField.paths','person.metadata'), ('requestMask.includeField.paths','person.name'), ('requestMask.includeField.paths','person.phone'), ('requestMask.includeField.paths','person.photo'), ('requestMask.includeField.paths','person.read_only_profile_info'), ('requestMask.includeField.paths','person.organization'), ('requestMask.includeField.paths','person.location'), ('requestMask.includeField.paths','person.cover_photo'), ('requestMask.includeContainer','PROFILE'), ('requestMask.includeContainer','DOMAIN_PROFILE'), ('requestMask.includeContainer','CONTACT'), ('key','REDACTED') ] response = requests.post('https://people-pa.clients6.google.com/v2/people/?key=REDACTED', headers={ 'X-HTTP-Method-Override': 'GET', 'Authorization': 'SAPISIDHASH REDACTED', 'Content-Type': 'application/x-www-form-urlencoded', 'Origin': 'https://hangouts.google.com', 'Cookie': 'REDACTED', }, data=urllib.parse.urlencode(data), ) print(response.text)
ฉันรันโปรแกรมนี้และใช้งานได้ – มันพิมพ์ JSON ออกมาจำนวนมาก! ไชโย!
คุณจะสังเกตเห็นว่าฉันได้แทนที่หลายอย่างด้วย REDACTED
นั่นเป็นเพราะถ้าฉันรวมค่าเหล่านั้นไว้ คุณจะสามารถเข้าถึง Google Hangouts API สำหรับบัญชีของฉันได้ ซึ่งคงจะไม่ดี
และเสร็จแล้ว!
ตอนนี้ ฉันสามารถปรับเปลี่ยนโปรแกรม Python ให้ทำทุกอย่างที่ต้องการได้ เช่น ส่งผ่านพารามิเตอร์ต่างๆ หรือแยกวิเคราะห์ผลลัพธ์
ฉันจะไม่ทำอะไรที่น่าสนใจกับมันเพราะฉันไม่สนใจที่จะใช้ API นี้เลย ฉันแค่ต้องการแสดงให้เห็นว่ากระบวนการนี้เป็นอย่างไร
แต่เราได้ JSON จำนวนหนึ่งกลับมาซึ่งคุณสามารถทำอะไรได้อย่างแน่นอน
curlconverter ดูดีมาก
มีคนแสดงความคิดเห็นว่าคุณสามารถแปล curl เป็น Python ได้ (และภาษาอื่นๆ อีกมาก!) โดยอัตโนมัติด้วย https://curlconverter.com/ ซึ่งดูดีมาก ฉันเคยทำด้วยตนเองมาโดยตลอด ฉันลองใช้ตัวอย่างนี้และดูเหมือนว่าจะใช้งานได้ดี
การหาว่า API ทำงานอย่างไรนั้นไม่สำคัญ
ฉันไม่ต้องการที่จะขีดเส้นใต้ว่ายากแค่ไหนที่จะเข้าใจว่า API ที่ไม่รู้จักทำงานอย่างไร – ไม่ชัดเจน! ฉันไม่รู้ว่าพารามิเตอร์ของ Google Hangouts API นี้ทำอะไรได้บ้าง!
แต่บ่อยครั้งที่มีพารามิเตอร์บางอย่างที่ดูตรงไปตรงมา เช่น requestMask.includeField.paths=person.email
อาจหมายถึง “รวมที่อยู่อีเมลของแต่ละคน” ดังนั้นฉันจึงพยายามมุ่งเน้นไปที่พารามิเตอร์ที่ ฉัน เข้าใจมากกว่าที่ฉัน ไม่ เข้าใจ
สิ่งนี้ใช้ได้เสมอ (ในทางทฤษฎี)
บางท่านอาจสงสัยว่า คุณทำสิ่งนี้ได้ตลอดเวลาหรือไม่
คำตอบคือใช่ – เบราว์เซอร์ไม่ใช่สิ่งมหัศจรรย์! เบราว์เซอร์ข้อมูลทั้งหมดที่ส่งไปยังแบ็กเอนด์ของคุณเป็นเพียงคำขอ HTTP ดังนั้น ถ้าฉันคัดลอกส่วนหัว HTTP ทั้งหมดที่เบราว์เซอร์ของฉันส่ง ไม่มีทางที่แบ็กเอนด์จะบอกได้อย่างแท้จริงว่าเบราว์เซอร์ของฉัน ไม่ได้ ส่งคำขอและถูกส่งโดยโปรแกรม Python แบบสุ่ม
แน่นอน เราได้ลบส่วนหัวจำนวนหนึ่งที่เบราว์เซอร์ส่งออกไป ดังนั้นในทางทฤษฎีแล้ว แบ็กเอนด์ สามารถ บอกได้ แต่โดยปกติแล้วจะไม่ตรวจสอบ
มีข้อแม้บางประการ เช่น บริการของ Google จำนวนมากมีแบ็กเอนด์ที่สื่อสารกับฟรอนต์เอนด์ด้วยวิธีที่ไม่อาจเข้าใจได้ (สำหรับฉัน) ดังนั้นแม้ว่าในทางทฤษฎีแล้ว คุณสามารถเลียนแบบสิ่งที่พวกเขาทำ แต่ในทางปฏิบัติอาจเกือบ เป็นไปไม่ได้.
ตอนนี้เราได้เห็นวิธีการใช้ API ที่ไม่มีเอกสารในลักษณะนี้แล้ว มาพูดถึงบางสิ่งที่อาจผิดพลาดกันได้
ปัญหาที่ 1: คุกกี้เซสชั่นหมดอายุ
ปัญหาใหญ่อย่างหนึ่งคือฉันใช้คุกกี้เซสชัน Google เพื่อตรวจสอบสิทธิ์ ดังนั้นสคริปต์นี้จะหยุดทำงานทุกครั้งที่เซสชันเบราว์เซอร์ของฉันหมดอายุ
นั่นหมายความว่าวิธีนี้ใช้ไม่ได้กับโปรแกรมที่ใช้เวลานาน (ฉันต้องการใช้ API จริง) แต่ถ้าฉันต้องการดึงข้อมูลเล็กน้อยอย่างรวดเร็วเพียงครั้งเดียวก็ใช้งานได้ดี !
ปัญหาที่ 2: การล่วงละเมิด
ถ้าฉันใช้เว็บไซต์ขนาดเล็ก มีโอกาสที่สคริปต์ Python ตัวน้อยของฉันสามารถหยุดบริการได้ เพราะมันส่งคำขอมากกว่าที่พวกเขาจะรับมือได้ ดังนั้นเมื่อฉันทำสิ่งนี้ ฉันพยายามที่จะเคารพและไม่ร้องขอมากเกินไปเร็วเกินไป
นี่เป็นสิ่งสำคัญโดยเฉพาะอย่างยิ่งเนื่องจากไซต์จำนวนมากที่ไม่มี API อย่างเป็นทางการเป็นไซต์ขนาดเล็กที่มีทรัพยากรน้อยกว่า
ในตัวอย่างนี้เห็นได้ชัดว่านี่ไม่ใช่ปัญหา ฉันคิดว่าฉันได้ส่งคำขอทั้งหมด 20 รายการไปยังแบ็กเอนด์ของ Google แฮงเอาท์ขณะเขียนบล็อกโพสต์นี้ ซึ่งพวกเขาสามารถจัดการได้อย่างแน่นอน
นอกจากนี้ หากคุณใช้ข้อมูลประจำตัวของบัญชีเพื่อเข้าถึง API ในลักษณะที่มากเกินไปและก่อให้เกิดปัญหา คุณอาจถูกระงับบัญชี (สมเหตุสมผลมาก)
ฉันยังยึดติดกับการดาวน์โหลดข้อมูลที่เป็นของฉันหรือตั้งใจให้เข้าถึงได้แบบสาธารณะ – ฉันไม่ได้ค้นหาช่องโหว่
อย่าลืมว่าทุกคนสามารถใช้ API ที่ไม่มีเอกสารของคุณได้
ฉันคิดว่าสิ่งสำคัญที่สุดที่ต้องรู้เกี่ยวกับเรื่องนี้ไม่ใช่วิธีใช้ API ที่ไม่มีเอกสาร ของ ผู้อื่น มันสนุกที่จะทำ แต่มีข้อจำกัดมากมาย และฉันไม่ได้ทำบ่อยขนาดนั้น
การเข้าใจว่าใครๆ ก็ทำสิ่งนี้กับแบ็กเอนด์ API ของคุณ ได้นั้นสำคัญกว่ามาก! ทุกคนมีเครื่องมือสำหรับนักพัฒนาและแท็บเครือข่าย และมันค่อนข้างง่ายที่จะดูว่าพารามิเตอร์ใดที่คุณส่งไปยังแบ็กเอนด์และเปลี่ยนแปลงได้
ดังนั้น ถ้าใครสามารถเปลี่ยนพารามิเตอร์บางอย่างเพื่อรับข้อมูลของผู้ใช้รายอื่นได้ ก็ไม่ดี ฉันคิดว่านักพัฒนาส่วนใหญ่ที่สร้าง API ที่เปิดเผยต่อสาธารณะรู้เรื่องนี้ดี แต่ฉันกำลังพูดถึงเพราะทุกคนจำเป็นต้องเรียนรู้มันเป็นครั้งแรกในบางประเด็น 🙂