معاينة الارتباط HTTP GET تكسر المواصفات

لقد كنت أواجه مشكلة منذ فترة ولم يُبلغ عنها أحد حتى الآن. أعتذر عن تعقيد الأمر، لكنني سأحاول وصفه بإيجاز.

باختصار شديد: عند لصق رابط في رسالة، ترسل مكتبة Ruby Gem التي تقوم في النهاية بطلب HTTP GET إلى ذلك الرابط للبحث عن بيانات التضمين طلبًا HTTP يُعتبر غير صالح وفقًا للمواصفات من قبل بعض وكلاء HTTP. وهذا يمنع عمل المعاينات في بعض الحالات:

والنسخة الأطول قليلًا هي كالتالي: نستخدم خدمة لطيفة تسمى Gitbook.io لتوثيقنا. Gitbook هي حل مستضاف ويستخدمون عمال Cloudflare لإعادة التوجيه الداخلي على موقعهم. تتضمن بعض عمال Cloudflare استخدام واجهة برمجة تطبيقات Node Fetch لتمرير طلبات HTTP. مطورو Node Fetch صارمون جدًا في اتباع المواصفات، وسيقومون برفض أي طلب GET يحتوي على جسم HTTP أو حتى رأس Content-Length، حتى لو كان هذا الرأس مضبوطًا على 0.
وهذا بالضبط ما يحدث. ترسل مكتبة Ruby التي تقوم بطلب HTTP رأس طلب

Content-Length: 0

وهذا يغضب وكيل Node Fetch بشدة وينتهي الأمر برفض الطلب من الخادم البعيد. كانت هناك مناقشات كثيرة في منتديات مختلفة حول ما إذا كان جسم الطلب على GET أو حتى مجرد رأس Content-Length صالحًا وفقًا لمواصفات HTTP. ليس لدي مشكلة في ذلك، لكن هذا لم يمنع مطوري Node Fetch من إغلاق كل مشكلة تم فتحها سابقًا تطلب منهم السماح بهذا الدلالي.

للأسف، أنا عالق في المنتصف هنا.

  • مشروع Node Fetch يرفض اعتبار هذه الطلبات HTTP صالحة.
  • دعم Cloudflare يرفض مساعدتي لأنني لا أتحكم في عمال Node المعنيين.
  • دعم Gitbook يرفض مساعدتي لأنهم يتفقون مع مطوري Fetch (ولست متأكدًا من اهتمامهم الحقيقي).
  • ومكتبة HTTPrb ترفض إزالة الرأس لأنهم يعتقدون أنه صالح تمامًا.

لذا، لا يسعني سوى النشر هنا والسؤال عما إذا كان هناك أي طريقة للتحكم في طلبات HTTP GET المُنشأة للمعاينات أو تغييرها لتشمل مجموعة مقبولة من رؤوس HTTP بحيث لا ترفض الوكلاء التي تستخدم مكتبات صارمة للغاية مثل Node Fetch هذه الطلبات؟

إذا أردت التجربة، إليك عنوان URL مثال يستضاف على خوادم Gitbook ويستخدم عامل Cloudflare المدعوم بـ Node Fetch الخاص بهم.

6 إعجابات

@jamie.wilson / @techAPJ هل لديكم أي فكرة عن سبب إرسالنا لـ Content-Length بقيمة 0 مع طلباتنا؟ هل يمكنكم تأكيد هذا السلوك؟ أعتقد أن هذا منطقي لـ HEAD، ولكن ماذا عن GET؟

إعجابَين (2)

مرحبًا @sam، يبدو أن طلبات HTTP تُنفَّذ بواسطة مكتبة Ruby تُسمى httprb، والتي تتسم بهذا السلوك. إذا نظرت إلى الرابط الموجود في النقطة التي تحمل عنوان “مكتبة HTTPrb ترفض إزالة الرأس لأنها تعتقد أنه صحيح تمامًا.”، يمكنك رؤية مطور المكتبة يقدّم حجةً تبرّر انحرافه عن مواصفات HTTP دون كسرها.

بما أنني كنت أبحث في كل مكان على الإنترنت لمحاولة إقناع شخص ما بالموافقة، تمكّنت من الحصول على طلب دمج (pull request) موجه إلى httprb قد يحلّ هذه المشكلة.

أنا لست مطوّر Ruby، لذا لا أعرف حتى كيفية اختبار هذا التعديل. أفترض أنه في النهاية سيصدر هذا الحزمة (Gem) إصدارًا يحتوي على التصحيح، ثم لاحقًا سيقوم Discourse بالتحديث لبدء استخدام هذا الإصدار. سيكون رائعًا لو كان هناك شخص ما يستطيع اختبار ما إذا كان يعمل. حالة التكرار بسيطة جدًا — ما عليك سوى لصق الرابط أعلاه إلى عنوان URL الخاص بـ Gitbook الخاص بي ومشاهدة ما إذا تم رفض المعاينة.

إعجاب واحد (1)

أرى ما يلي (وهو يتطابق مع الصورة في منشورك الأول):

النص “دليل البدء” يُظهر لنا أن الطلب كان ناجحًا — فهو يستخرج هذه السلسلة من وسم الميتا og:title:

<meta property="og:title" content="Getting Started Guide" data-react-helmet="true">

الخطأ/التنبيه بشأن عدم وجود وصف هو أيضًا صحيح. محتوى الصفحة هو كما يلي:

<meta property="og:description" content="" data-react-helmet="true">

رابط الصورة يأتي من وسم og:image، وهو كالتالي:

<meta data-react-helmet="true" property="og:image" content="https://app.gitbook.com/share/space/thumbnail/-LA-UVvV3_TgzQyCXMWK.png">

إذا قمت بنسخ ولصق https://app.gitbook.com/share/space/thumbnail/-LA-UVvV3_TgzQyCXMWK.png في متصفحي (Safari حديث على MacOS)، فإنني أحصل على خطأ يقول:

Error: could not handle the request

عند إجراء نفس الطلب عبر curl، أحصل على نفس الاستجابة:

curl -v https://app.gitbook.com/share/space/thumbnail/-LA-UVvV3_TgzQyCXMWK.png
*   Trying 104.18.8.111...
* TCP_NODELAY set
* Connected to app.gitbook.com (104.18.8.111) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/cert.pem
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-ECDSA-CHACHA20-POLY1305
* ALPN, server accepted to use h2
* Server certificate:
*  subject: C=US; ST=California; L=San Francisco; O=Cloudflare, Inc.; CN=sni.cloudflaressl.com
*  start date: Jun 16 00:00:00 2021 GMT
*  expire date: Jun 15 23:59:59 2022 GMT
*  subjectAltName: host "app.gitbook.com" matched cert's "*.gitbook.com"
*  issuer: C=US; O=Cloudflare, Inc.; CN=Cloudflare Inc ECC CA-3
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x142809200)
> GET /share/space/thumbnail/-LA-UVvV3_TgzQyCXMWK.png HTTP/2
> Host: app.gitbook.com
> User-Agent: curl/7.64.1
> Accept: */*
>
* Connection state changed (MAX_CONCURRENT_STREAMS == 256)!
< HTTP/2 500
< date: Mon, 23 Aug 2021 17:40:04 GMT
< content-type: text/plain; charset=utf-8
< content-length: 36
< cf-ray: 68361fb8ea9b4009-YYZ
< age: 432
< vary: Accept-Encoding
< via: magic cache
< cf-cache-status: HIT
< expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
< x-cache: HIT
< x-cloud-trace-context: 9d4cbd24a15138451c88b2ced35a32f1;o=1
< x-content-type-options: nosniff
< x-magic-hash: f46ac4bf6b6dc125a68e9ad566b48481631bb27eec2165532a7c0f538e93c4f6
< x-release: gitbook-28427-6.25.14
< server: cloudflare
<
Error: could not handle the request
* Connection #0 to host app.gitbook.com left intact
* Closing connection 0

هل يمكنك رؤية صورة إذا قمت بنسخ ولصق رابط og:image في متصفحك؟

باختصار: معاينة Onebox تبدو وكأنها تعمل كما هو متوقع، بناءً على الاستجابة من الرابط الأصلي.

3 إعجابات

@jamie.wilson شكرًا لك على وقتك في التحقق من هذا. هل يمكنك التوضيح، ومع ذلك، ما إذا كان اختبارك أعلاه قد تم باستخدام أحدث إصدار من مكتبة httprb الذي يتضمن طلب السحب المذكور سابقًا، أم أنه الإصدار الأقدم من المكتبة؟

كانت رسالة الخطأ الأصلية التي كنت أراها في معاينة Onebox هي أن عنوان URL المستهدف قد أعاد رمز حالة 500. وفي مرحلة ما قبل نشر هذا المنشور، بدأت معاينة Onebox في إظهار الملاحظة المتعلقة ببيانات opengraph المفقودة بدلاً من ذلك. وبما أنني كنت أقوم باستكشاف هذه المشكلة وإصلاحها لشهور قبل النشر مع دعم Gitbook، فمن الممكن أن يكون قد حدث تغيير ما في هذه الأثناء.

إذا كان عنوان URL الخاص بـ Gitbook يُحمّل فعليًا ولكنه يفتقر فقط إلى بعض البيانات الوصفية أو يحتوي على صور مفقودة، فإن هذا يختلف عن رفض الطلب. ومع ذلك، فأنا أعرف بالتأكيد أن أي طلبات أرسلها بنفسي تحتوي على رأس طلب HTTP Content-Length: 0 يتم رفضها بواسطة عمال CloudFlare على الخادم البعيد. ربما يكون عميل HTTP المستخدم لإرسال الطلبات في Discourse قد تغير؟ أنا لا أعرف شيئًا عن كود مصدر Discourse، وأنا لست حتى متأكدًا بنسبة 100% أن مكتبة httprb هي المصدر الفعلي لطلبات HTTP.

لا أعتقد أننا نستخدم مكتبة httprb على الإطلاق. عملية Oneboxing (أي العملية التي تولد معاينات الروابط) تستخدم Net::HTTP من مكتبة Ruby القياسية، ومكتبة Excon أيضًا كجزء من سير العمل.

عند التعمق أكثر، أستطيع أن أرى أننا نقوم أحيانًا بتوليد طلبات مع رأس Content-Length: 0. ومع ذلك، في حالة عنوان URL المقدم على الأقل، فإن هذا لا يتعارض مع توليد Onebox.

قد يكون هناك تحديث طفيف في الإصدار، لكن لا شيء كبير مثل إعادة هندسة طريقة إجراء الطلبات أو المكتبات التي نستخدمها لذلك.

كانت هناك بعض التغييرات لجعل عملية Oneboxing أكثر قوة بشكل عام، وهو ما قد يفسر سبب نجاح Oneboxing لعناوين URL كانت تعطي خطأ 500 سابقًا.

إذا كانت لديك عناوين URL يمكنك مشاركتها وتعيد أخطاء حاليًا أثناء عملية Oneboxing (أو لا تعمل كما هو متوقع في جزء آخر من Discourse)، فلا تتردد في إرسالها إلي!

3 إعجابات

آه، إذن هذه معلومات ممتازة. لقد اضطررتُ إلى تقديم تخمينات عشوائية حول المكتبات المشاركة حتى هذه النقطة، ويرجع ذلك إلى حد كبير إلى عدم تلقي أي مساعدة تقريبًا من فريق Gitbook الذي يدير وكلاء CloudFlare.

فهمت. لا أعتقد أنني شاركت ما سبق، ولكن المعلومة الوحيدة التي تمكّنتُ من الحصول عليها من Gitbook كانت أن الخطأ في سجلات أخطاء CloudFlare الخاص بهم والذي كان يرفض طلبات المعاينة من Discourse هو:

لا يمكن أن يحتوي الطلب باستخدام طريقة GET أو HEAD على جسم.

ما ليس واضحًا هو ما إذا كان طلب GET الصادر من Discourse يحتوي فعليًا على جسم أم أنه يحتوي فقط على رأس Content-Length: 0. وعلى أي حال، فإن ذلك ينتهك مواصفات Fetch وفقًا لبعض الأشخاص (بما في ذلك الأشخاص في Cloudflare)

نعم، في مرحلة ما يبدو أن خطأ Onebox تغير من خطأ 500 العام إلى احتوائه الآن على بعض البيانات. لا يمكن الجزم بأي المكتبات قد تم تحديثها (وقد قمتُ بتحديث Discourse خلال هذه الفترة). أتمنى لو كان لديّ طريقة لالتقاط الرؤوس المرسلة بالضبط من Discourse، ولكن حتى لو قمتُ بزيارة عنوان URL مثل http://httpbin.org/get، فلا توجد لديّ طريقة “لرؤية” ما يتم إرجاعه لأن النتائج يتم استهلاكها بالكامل بواسطة Onebox ولا يتم تسجيلها في أي مكان أعرفه.

إذا اختفى رأس content-length الفارغ الآن، فيمكنني على الأقل العمل مع Gitbook لإصلاح نظام التضمين الخاص بهم (وهو ما لن يحدث لأنهم يعيدون كتابة تطبيقهم بالكامل من الصفر ويرفضون معالجة أي أخطاء موجودة حاليًا :confused: ولكن هذا على الأقل ليس مشكلة Discourse)

دعني أولاً أقول إن الكثير مما كُتب أعلاه يفوق فهمي بكثير، لكنني هنا أحاول التمسك بأي غصن. لا بأس على الإطلاق بإخباري أنني أخطأت إذا كنت أتدخل في موضوع خاطئ.

نواجه هذه المشكلة بشكل متكرر، لأننا ننشر روابط لمركز المساعدة لدينا (قاعدة المعرفة) في مجتمعنا بشكل متكرر.

بعض الأمثلة على الروابط التي تفشل في عملية Oneboxing:

https://help.republicwireless.com/hc/en-us/articles/115014150828--How-to-Add-an-E911-Address

من لوحة المعاينة أثناء الكتابة:

إعجاب واحد (1)

بعد الغوص بعمق أكبر، يتضح أن مكتبة Excon هي التي تضيف Content-Length: 0، لكن ليس في طلبات GET.

لكن هذا الكود موجود منذ 8 أو 9 سنوات، لذا فمن المرجح أنه لم يكن هو المشكلة.

سيُظهر لك ملف Gemfile.lock المكتبات (gems) التي يستخدمها نواة Discourse.

إعجابَين (2)

هذا الموقع خلف اختبار كابتشا من كلاودفلير، وهو يمنع ديسكورد من جلب أي معلومات منه :slightly_frowning_face:

إعجابَين (2)

عند عرض هذه الصفحة في متصفح، تحتوي بالفعل على وسوم الميتا الضرورية لبناء Onebox. ومع ذلك، عند محاولة جلب الرابط، يبدو أننا نتلقى خطأ!

oneboxer preview url: https://help.republicwireless.com/hc/en-us/articles/115014150828--How-to-Add-an-E911-Address
headers: {"User-Agent"=>"Discourse Forum Onebox v2.8.0.beta4"}
helpers response code: 403

وهذا يعني أننا طلبنا هذا الرابط باستخدام وكيل مستخدم (User Agent) هو “Discourse Forum Onebox v2.8.0.beta4”، لكن خادم الويب البعيد رد بـ رمز حالة 403.

وبالمثل، باستخدام أداة سطر الأوامر wget:

wget https://help.republicwireless.com/hc/en-us/articles/115014150828--How-to-Add-an-E911-Address
--2021-08-23 17:38:30--  https://help.republicwireless.com/hc/en-us/articles/115014150828--How-to-Add-an-E911-Address
Resolving help.republicwireless.com (help.republicwireless.com)... 104.16.53.111, 104.16.51.111
Connecting to help.republicwireless.com (help.republicwireless.com)|104.16.53.111|:443... connected.
HTTP request sent, awaiting response... 403 Forbidden

وهو ما يقول نفس الشيء… نحن نرسل طلبًا صالحًا، لكن خادم الويب البعيد يرفض إرجاع نتيجة. هل من الممكن للأشخاص المسؤولين عن help.republicwireless.com إزالة الحظر عن هذه الطلبات الصالحة؟

هذان الموقعان لا يحتويان على وسوم عنوان/وصف OpenGraph، ومع ذلك فهما يحتويان على عناوين/أوصاف أخرى يجب أن يعتمد عليها Onebox كخيار احتياطي. هذا شيء يجب أن نعمل على إصلاحه.

إعجابَين (2)

:confused: لكنه كان يعمل بشكل صحيح منذ سنوات.

إليك مثال حيث يعرض رابط من نفس الموقع صندوق معلومات صالح: https://forums.republicwireless.com/t/4-digit-pin-which-i-have-forgot/37655/2

الذي يوجه إلى https://help.republicwireless.com/hc/en-us/articles/115012101188-Can-t-Get-Past-the-Screen-Lock-on-the-Phone

إعجاب واحد (1)

تقوم Cloudflare بتغيير خوارزمية كشف الروبوتات الخاصة بها باستمرار، لذا إذا كنت تريد أن لا يتم حظر Discourse، فقد ترغب في الاتصال بدعمهم والسبب في حظر الطلب.

5 إعجابات

عذرًا أيها الزملاء، سنغلق هذا الموضوع لعدم نشاطه، وسنحتاج إلى إعادة إنتاج للمشكلات لفتحها.