من الفكرة إلى الحزمة التي تراها في Wireshark


كيف بُني SWP؟ قصة البروتوكول، معماريته، وما الذي يحدث فعلًا بين التطبيق والشبكة

#1. الفكرة باختصار

هناك فرق كبير بين أن تستخدم الشبكة، وبين أن ترى ما تفعله الشبكة فعلًا. في معظم التطبيقات الحديثة يكفي أن تستدعي دالةً ترسل طلبًا، فتعود إليك استجابة، بينما تختفي بين اللحظتين تفاصيل كثيرة: كيف تحوّل المعنى إلى بايتات؟ كيف يعرف الطرف الآخر أين تنتهي الرسالة؟ وماذا يحدث إذا وصل نصفها الآن والنصف الآخر لاحقًا؟

من هذه الأسئلة وُلد Sawlah Wire Protocol (SWP)؛ بروتوكول ثنائي صغير يعمل في طبقة التطبيق فوق TCP، صممته ونفذته بحيث يتبادل العميل والخادم رسائل صريحة ذات بنية معرّفة، بدل الاعتماد على بروتوكول نصي مثل HTTP.

كانت الفكرة أن تصبح كل مرحلة من رحلة الرسالة مرئية وقابلة للفهم:

  1. ينشئ التطبيق بيانات منظمة.
  2. تُشفَّر هذه البيانات إلى بايتات.
  3. ينقل TCP تلك البايتات بوصفها دفقًا مرتبًا.
  4. يعيد الطرف المستقبل بناء الرسائل الكاملة من الدفق.
  5. يتحقق من سلامة الرسالة، ثم يوجهها إلى العملية المناسبة.

ولهذا يتضمن SWP عددًا من العناصر التي تجعل هذه الرحلة قابلة للمشاهدة والتجربة:

  • تنسيقًا ثنائيًا ثابتًا للإطار Frame.
  • تأطيرًا يعتمد على طول الحمولة، حتى يمكن إعادة تجميع دفق TCP بصورة صحيحة.
  • ترميز الأعداد متعددة البايتات بترتيب big-endian، كما تفعل بروتوكولات الشبكات المعتادة.
  • فحص سلامة باستخدام CRC32.
  • رسائل طلب واستجابة مرتبطة بمعرّفات تسلسل Sequence IDs.
  • عميلًا وخادمًا متعدد الخيوط مكتوبين بمكتبة Python القياسية فقط.
  • أوامر حالة محددة مسبقًا ضمن قائمة سماح.
  • بث محادثة اختياريًا.
  • رفع ملفات اختياريًا مع التحقق من الحجم وSHA-256.
  • مصادقة اختيارية بمفتاح مشترك.
  • تشفيرًا اختياريًا عبر TLS.
  • محللًا لـWireshark مكتوبًا بلغة Lua.
  • اختبارات وحدات وتكامل تتحقق من صيغة البيانات على الشبكة ومن سلوك الاتصال نفسه.

يُشار إلى النواة الأصلية في التوثيق باسم SWP v1.0، أما الخصائص التي أضيفت لاحقًا فيشار إليها باسم امتدادات v1.1. ومع ذلك بقي إصدار الصيغة على الشبكة عند القيمة نفسها 0x01، لأن بنية الحزمة الأساسية وأنواع الرسائل الأصلية لم تتغير.

الحالة: هذا مشروع تعليمي وتنفيذ مرجعي صُمم لجعل مفاهيم بروتوكولات الشبكات واضحة وسهلة الفحص. وهو ليس بروتوكولًا أمنيًا مخصصًا للاستخدام الإنتاجي.


#2. لماذا أنشأت SWP؟

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

لهذا أنشأت SWP بوصفه مختبرًا صغيرًا لتلك التفاصيل؛ مشروعًا واحدًا أستطيع فيه أن أختار كل بايت في تنسيق الرسالة، وأرى الفرق بين دفق TCP وبين رسالة التطبيق، وأنفذ طرفي الاتصال بنفسي، ثم أتعمد تقسيم الحزم أو دمجها لأختبر قدرة المستقبل على التعامل معها. وأردت أيضًا أن أرى الحركة الحقيقية في Wireshark، وأن أفهم كيف تكشف قيم التحقق الأخطاء العرضية، وأن أضيف وظائف مفيدة من دون أن أخفي البروتوكول تحت طبقات جاهزة، مع إبقاء الشفرة صغيرة بما يكفي لأن يستطيع شخص واحد قراءتها وشرحها كاملة.

لم تكن الفكرة أن أخترع بديلًا لـHTTP أو SSH أو FTP أو لأي معيار ناضج آخر. الفكرة كانت أبسط وأكثر فائدة للتعلم: بناء بروتوكول كامل، مفهوم، يبدأ من الـSocket صعودًا. صغير بما يكفي للدراسة، لكنه متكامل بما يكفي لعرض أهم قضايا هندسة البروتوكولات: التأطير، والتحقق، والحالة، والأخطاء، والتزامن، والأمن، وقابلية الرصد.

#2.1 المسألة التصميمية

كانت المسألة التي ينبغي على المشروع حلها واضحة:

كيف يمكن لعميل وخادم تبادل أنواع مختلفة من الرسائل بصورة موثوقة فوق اتصال TCP، مع إبقاء كل رسالة قابلة للفحص والفهم على مستوى البايت؟

ولكي ينجح ذلك، كان على البروتوكول أن يميز -على الأقل- بين العمليات الآتية:

  • بدء جلسة.
  • إرسال نص.
  • طلب تنفيذ أمر مضبوط.
  • إعادة نتيجة الأمر.
  • التحقق من أن الاتصال لا يزال حيًا.
  • الإبلاغ عن الأخطاء.
  • إغلاق الاتصال بصورة سليمة.

بعد أن استقرت هذه النواة، توسع البروتوكول ليشمل رفع الملفات، والمحادثة، والمصادقة، وTLS. أضيفت هذه الوظائف عمدًا على هيئة رسائل داخل البروتوكول نفسه، لا كقنوات جانبية منفصلة، حتى يمكن إعادة استخدام المشفّر، وفاكّ الترميز، وقواعد التسلسل، وأدوات Wireshark ذاتها.

#2.2 لماذا TCP؟

اخترت TCP لأنه يقدم:

  • تسليمًا موثوقًا.
  • بايتات مرتبة.
  • إعادة إرسال للمقاطع المفقودة.
  • تحكمًا بالازدحام.
  • نموذج اتصال بين عميل وخادم قائمًا على جلسة اتصال.

وهذا يجعله طبقة نقل مناسبة لبناء أول بروتوكول تطبيقي. لكنه يفرض علينا في الوقت نفسه الدرس الأهم في هذا المشروع: TCP لا يحافظ على حدود رسائل التطبيق.

بعبارة أخرى، إذا أرسل التطبيق رسالتين منفصلتين، فلا توجد قاعدة تقول إن الطرف الآخر سيستقبلهما في عمليتي recv() منفصلتين. لذلك يجب أن يعرّف البروتوكول طريقته الخاصة في تحديد حدود الرسائل، وهذه الحاجة إلى التأطير هي أحد الأسباب الأساسية التي بُني من أجلها SWP.

كان من الممكن استخدام UDP، لكن ذلك كان سيجبر SWP على تصميم الاعتمادية والترتيب ومعالجة الفقد وإعادة الإرسال وربما التحكم في الازدحام. وكان من الممكن استخدام HTTP، لكن كثيرًا من مسائل التأطير والتعامل مع الرسائل كانت ستصبح مخفية داخل المعيار. أما TCP فقدم حلًا وسطًا مناسبًا: طبقة النقل موثوقة، بينما تبقى صياغة رسائل التطبيق مسؤوليتنا نحن.


#3. الأهداف وما لا يهدف إليه المشروع

#3.1 الأهداف

صُمم SWP ليكون:

  1. سهل القراءة — لكل حقل معنى بسيط، والشفرة تسير بمحاذاة تنسيق البيانات الفعلي على الشبكة.
  2. صريحًا — يظهر نوع الرسالة، وطول الحمولة، ومعرّف التسلسل بوضوح على السلك.
  3. صالحًا لدفق TCP — يستطيع المستقبل التعامل مع قراءات مجزأة أو مدمجة.
  4. قابلًا للفحص — يمكن طباعة البايتات الخام وتحليل الحركة في Wireshark.
  5. قابلًا للاختبار — توجد اختبارات منفصلة للترميز، وفك الترميز، وسلوك التكامل.
  6. صغيرًا — يعتمد على مكتبة Python القياسية فقط، بلا اعتمادات تشغيل خارجية.
  7. قابلًا للتوسعة — يمكن إضافة أنواع رسائل جديدة مع إبقاء إصدار الصيغة ثابتًا متى بقي التأطير متوافقًا.

#3.2 ما لا يهدف إليه

SWP ليس مصممًا ليكون:

  • بديلًا لـHTTP أو واجهة Web API عامة.
  • نظام مزامنة ملفات إنتاجيًا.
  • Shell أو بروتوكول تنفيذ أوامر عن بعد.
  • نظام هوية وإدارة وصول متكاملًا.
  • بديلًا لـTLS.
  • بروتوكولًا عالي الأداء متعدد الإرسال multiplexed.
  • بروتوكولًا يضم حماية إنتاجية من إعادة التشغيل Replay أو حدودًا متقدمة للمعدلات أو جدولة الموارد.

ولهذا يستخدم التنفيذ عمدًا قائمة سماح ثابتة للأوامر، ولا يمرر أي أمر إلى Shell نظام التشغيل.


#4. أساسيات الشبكات التي يقوم عليها المشروع

قبل الدخول في تفاصيل SWP، من المفيد أن نفصل بين الطبقات التي تشترك في أي اتصال اعتيادي.

#4.1 الطبقات في هذا المشروع

الطبقةالمسؤوليةمثال داخل SWP
Applicationتحدد معنى الرسائلCOMMAND PING, FILE_CHUNK, ACK
Transportتنقل البايتات بين الطرفينTCP
Networkتوجه الحزم بين عناوين IPIPv4/IPv6 أو توجيه Loopback
Linkتنقل الإطارات على الوسط المحليEthernet أو Wi-Fi أو Loopback

يعيش SWP في طبقة التطبيق. فهو لا يستبدل TCP أو IP، وإنما يكتب بايتات إلى Socket من نوع TCP ويقرأ البايتات العائدة من هذا الـSocket.

ولهذا يمكن للإطار نفسه من SWP أن يمر عبر:

  • 127.0.0.1 على الجهاز نفسه باستخدام واجهة Loopback.
  • عنوان خاص داخل شبكة LAN عبر Ethernet أو Wi-Fi.
  • عنوان موجه يمر عبر أكثر من شبكة.

التطبيق يرى Socket، أما الطبقات الأدنى فتتولى تفاصيل النقل.

#4.2 العناوين والمنافذ والـSockets

عادة ما نعرّف نقطة الاتصال بعنوان IP ومنفذ:

text
127.0.0.1:9320
  • 127.0.0.1 هو عنوان IPv4 Loopback، أي «هذا الجهاز نفسه».
  • 9320 هو منفذ TCP الافتراضي المختار لخادم SWP.
  • المنفذ يحدد نقطة خدمة التطبيق داخل المضيف.

يمر الخادم بالتسلسل الآتي:

text
socket() -> bind(address, port) -> listen() -> accept()

أما العميل فيمر بالتسلسل:

text
socket() -> connect(server_address, port)

بعد اكتمال accept() وconnect() يمتلك الطرفان Socket TCP متصلًا. عندها تُرسل إطارات SWP عبر sendall() وتُقرأ عبر recv().

#4.3 TCP دفق بايتات، وليس واجهة حزم

لنفترض أن العميل أرسل إطارين كاملين:

text
sendall(FRAME_A)
sendall(FRAME_B)

يستطيع الخادم -وهذا طبيعي تمامًا- أن يرى القراءات بالشكل الآتي:

text
recv() # 1: النصف الأول من FRAME_A
recv() # 2: النصف الثاني من FRAME_A ومعه جزء من FRAME_B
recv() # 3: بقية FRAME_B

وقد يستقبل الإطارين كاملين في نداء واحد أيضًا. نداءات sendall() عند المرسل لا تتحول إلى حدود رسائل عند المستقبل.

ولهذا يضع SWP طول الحمولة داخل كل إطار. يحتفظ المستقبل بالبايتات في Buffer حتى يصبح لديه:

text
header + declared payload + CRC32

وعند اكتمال ذلك فقط ينتج كائن Packet واحدًا على مستوى التطبيق.

#4.4 الاعتمادية لا تعني الأمن

يوفر TCP تسليمًا موثوقًا ومرتبًا بين طرفي اتصال TCP، لكنه لا يوفر:

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

لهذا يستخدم SWP CRC32 لاكتشاف التلف العرضي، وقائمة سماح للأوامر، ومصادقة اختيارية بمفتاح مشترك، وTLS اختياريًا للسرية ومصادقة الخادم.

#4.5 حركة Loopback والتقاط الحزم

عندما يعمل الطرفان على 127.0.0.1 لا تغادر الحركة الجهاز أصلًا، ولذلك لن تمر عبر كرت Ethernet أو Wi-Fi. لالتقاطها يجب الاستماع على واجهة Loopback المناسبة:

نظام التشغيلالواجهة
Linuxlo
macOSlo0
WindowsNpcap Loopback Adapter

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


#5. معمارية SWP

قُسم المشروع إلى مكتبة بروتوكول، وعميل، وخادم، وأدوات، واختبارات، ومحلل خاص بـWireshark.

text
                         +----------------------+
                         |      Wireshark       |
                         |      swp.lua         |
                         +----------+-----------+
                                    |
                                    | observes TCP/TLS traffic
                                    v
+----------------+       TCP       +----------------+
|  SWP client    | <--------------> |  SWP server    |
| client/client  |                  | server/server  |
+-------+--------+                  +--------+-------+
        |                                    |
        +---------------+--------------------+
                        |
                        v
              +---------------------+
              | protocol/           |
              | Packet              |
              | encode              |
              | FrameDecoder        |
              | CRC32 and constants |
              +---------------------+

#5.1 مكوّنات المستودع

المسارالمسؤولية
protocol/constants.pyقيمة Magic، الإصدار، الحدود، أنواع الرسائل، Flags، وتنسيق الترويسة الثنائية
protocol/packet.pyنموذج البيانات غير القابل للتغيير Packet ومساعدات UTF-8
protocol/encoder.pyتحويل Packet إلى بايتات على الشبكة
protocol/decoder.pyفك ترميز دفق TCP تدريجيًا والتحقق منه
protocol/checksum.pyدالة مساعدة لـCRC32
protocol/exceptions.pyأنواع أخطاء البروتوكول والترميز
client/client.pyالعميل التفاعلي/التجريبي وعميل نقل الملفات
server/server.pyخادم TCP متعدد الخيوط، Dispatcher، المحادثة، المصادقة، والرفع
wireshark/swp.luaمحلل Wireshark
tools/packet_dump.pyترميز إطار واحد أو فكّه لأغراض الفحص
tools/capture_demo.shمساعد اختياري لالتقاط الحركة المحلية
tests/اختبارات الوحدات والتكامل والخصائص وTLS
docs/SWP_SPEC.mdالمواصفة الرسمية على مستوى الـWire

#5.2 مسار البيانات من البداية إلى النهاية

يمر الطلب الاعتيادي بالمسار الآتي:

text
Application call
    |
    v
Packet(type, sequence, payload, flags)
    |
    v
encode() -> header + payload + CRC32
    |
    v
TCP sendall()
    |
    v
TCP recv() may return any-sized byte fragment
    |
    v
FrameDecoder buffer and reassembly
    |
    v
CRC and field validation
    |
    v
Packet
    |
    v
Server dispatch or client reply handling

هذا الفصل بين الطبقات مهم. فالمشفّر وفاك الترميز يهتمان بالبايتات، بينما يفهم العميل والخادم معنى الرسائل. عندما تبقى هاتان المسؤوليتان منفصلتين يصبح البروتوكول أسهل في الاختبار والتوسعة.


#6. تنسيق إطار SWP

كل رسالة في SWP هي إطار واحد:

text
+----------------------+-------------------+----------------+
| 13-byte header       | N-byte payload    | 4-byte CRC32   |
+----------------------+-------------------+----------------+

الحجم الكلي للإطار هو:

text
13 + payload_length + 4 = 17 + payload_length bytes

أصغر إطار ممكن لا يحمل أي Payload، ولذلك يبلغ حجمه 17 بايت. أما الحد الأعلى للحمولة فهو 1 MiB، أي 1,048,576 بايت، ليصبح الحد الأقصى لحجم الإطار كاملًا 1,048,593 بايت.

#6.1 حقول الترويسة

جميع الأعداد متعددة البايتات تستخدم ترتيب big-endian، المعروف أيضًا باسم Network Byte Order.

OffsetSizeالحقلالمعنى
02MagicASCII SW، أي البايتان 0x53 0x57
21Versionإصدار الـWire وهو 0x01
31Message typeقيمة رقمية من جدول أنواع رسائل SWP
41Flags0x00 عادةً؛ والبت 0x01 يعني Chat Broadcast في الامتداد
54Payload lengthحجم الحمولة كعدد غير موقّع، بحد أقصى 1 MiB
94Sequence IDرقم غير موقّع لربط الطلب باستجابته
13NPayloadعدد البايتات نفسه المعلن في حقل الطول
13 + N4CRC32قيمة تحقق Big-endian محسوبة على الترويسة والحمولة

تعريف ترويسة الـ13 بايت في Python هو:

python
HEADER_STRUCT = struct.Struct("!2sBBBII")

تشير ! إلى Network Byte Order، أما الحقول فهي:

text
2s  magic: two raw bytes
B   version: one unsigned byte
B   type: one unsigned byte
B   flags: one unsigned byte
I   payload length: four-byte unsigned integer
I   sequence: four-byte unsigned integer

#6.2 لماذا توجد قيمة Magic؟

البايتان SW يحددان بداية إطار SWP، ويسهّلان التعرف إلى البروتوكول في Capture خام أو أثناء التصحيح. ومع ذلك لا يستخدم الـDecoder هذه القيمة للبحث عن إطار لاحق بعد اكتشاف خطأ. فإذا أصبح إطار ما غير صالح، يُعامل الدفق كله على أنه غير آمن للاستمرار لأن فاك الترميز قد لا يعود قادرًا على تحديد موضع بداية الإطار التالي بثقة.

#6.3 لماذا يوجد حقل Version؟

وجود الإصدار يسمح للتنفيذ برفض إطار لا يفهمه بدل تفسير حقوله بصمت وفق معانٍ خاطئة. التنفيذ الحالي يقبل الإصدار 1 فقط.

أما خصائص v1.1 فتستخدم إصدار الـWire نفسه 1 لأنها أضافت أنواع رسائل وFlag جديدًا من دون تغيير بنية الترويسة. وإذا تغيرت الترويسة مستقبلًا بصورة غير متوافقة، فينبغي استخدام إصدار Wire جديد.

#6.4 لماذا يوجد حقل Length؟

حقل الطول هو المفتاح إلى إعادة بناء الرسائل فوق TCP. بدونه لن يعرف المستقبل أين تنتهي الحمولة ولا أين يبدأ الإطار التالي.

وهو يتيح أيضًا رفض حمولة ضخمة فور قراءة الترويسة فقط، بدل تخصيص الذاكرة أو انتظار Payload عدائي يزيد على حد البروتوكول.

#6.5 لماذا يوجد Sequence ID؟

يبدأ العميل بالرقم 1 ويزيده مع كل طلب. ينسخ الخادم القيمة نفسها في الاستجابة:

text
request sequence  ->  response sequence
        7         ->          7

وهكذا يستطيع العميل ربط كل استجابة بالعملية التي سببتها. العميل المرجعي الحالي ينفذ طلبًا متزامنًا واحدًا في كل مرة، لكن وجود Sequence ID يترك الباب مفتوحًا مستقبلًا لدعم عدة طلبات معلّقة في الوقت نفسه.

أما إطارات المحادثة التي يدفعها الخادم من تلقاء نفسه فتختلف: تستخدم Sequence 0 مع Broadcast Flag. وعلى العميل ألا يفسر هذه الرسائل غير المطلوبة على أنها ردود على طلباته.

#7. ترميز الإطار

يتبع المشفّر الموجود في protocol/encoder.py سلسلة واضحة من الخطوات:

  1. يتحقق من أن حجم الحمولة لا يتجاوز 1 MiB.
  2. يتحقق من أن قيمة Sequence تقع ضمن مجال عدد صحيح غير موقّع من 32 بت.
  3. يتحقق من أن Flags تقع ضمن مجال عدد غير موقّع من 8 بت.
  4. يتحقق من أن نوع الرسالة معروف.
  5. يعبئ الترويسة بترتيب Big-endian.
  6. يضيف الحمولة.
  7. يحسب CRC32 على الترويسة والحمولة معًا.
  8. يضيف قيمة CRC32 في آخر الإطار على هيئة أربعة بايتات Big-endian.

مفاهيميًا، يمكن اختصار العملية إلى:

python
header = pack(magic, version, message_type, flags, len(payload), sequence)
body = header + payload
frame = body + pack_big_endian_uint32(crc32(body))

يمثل كائن Packet الرسالة المنطقية قبل تحويلها إلى صيغة البايتات:

python
Packet(
    type=MessageType.COMMAND,
    sequence=4,
    payload=b"GET_TIME",
    flags=0,
    version=1,
)

تستخدم المساعدات النصية ترميز UTF-8. ولا توجد Null Terminator في نهاية النص؛ فحقل الطول هو الذي يحدد النهاية. أما الرسائل الثنائية مثل FILE_CHUNK فتحمل bytes الخام مباشرة.


#8. فك الترميز وإعادة تجميع دفق TCP

يعمل FrameDecoder بصورة تدريجية. يمكن تزويده بأي كمية من البايتات تعيدها recv()، وقد يرجع صفرًا أو إطارًا واحدًا أو عدة إطارات كاملة.

#8.1 خوارزمية الـDecoder

text
append newly received bytes to an internal buffer

while the buffer contains at least 13 bytes:
    read and validate the header
    reject bad magic, version, type, or oversized length
    calculate total_frame_size = 13 + payload_length + 4

    if the complete frame is not buffered yet:
        wait for the next recv()

    remove one complete frame from the buffer
    calculate CRC32 over its header and payload
    compare it with the received CRC32
    emit one Packet

continue if another complete frame remains in the buffer

الفكرة هنا أن الـDecoder لا يربط بين نداء recv() ورسالة واحدة. إنه يبني الرسائل من دفق البايتات وفق القواعد التي يحددها البروتوكول.

#8.2 الإدخال المجزأ

هذا السيناريو صالح تمامًا:

python
decoder.feed(frame[:7])
# returns []

decoder.feed(frame[7:])
# returns [decoded_packet]

يمكن أن ينقسم الإطار عند أي بايت؛ داخل الترويسة، أو الحمولة، أو حتى في منتصف CRC.

#8.3 الإدخال المدمج

وهذا صالح أيضًا:

python
decoder.feed(frame_a + frame_b)
# returns [packet_a, packet_b]

فالـDecoder يكرر المعالجة على محتوى الـBuffer بدل أن يفترض أن كل recv() تساوي Packet واحدة.

#8.4 ترتيب التحقق

يتحقق المستقبل من الترويسة قبل الانتظار لبقية الحمولة، وفق الترتيب الآتي:

  1. أن تكون Magic هي SW.
  2. أن يكون الإصدار مدعومًا.
  3. أن يكون نوع الرسالة معروفًا.
  4. ألا يتجاوز Payload Length مقدار 1 MiB.
  5. أن يكون الإطار كاملًا موجودًا في الـBuffer.
  6. أن تتطابق قيمة CRC32.

إذا خالف الإطار صيغة الـWire، فلا يعود من الآمن افتراض أن الدفق ما زال متزامنًا مع حدود الإطارات. عندها يرسل الخادم ERROR باستخدام Sequence 0 إذا تعذر ربط الخطأ بطلب محدد، ثم يغلق الاتصال.

لكن المشكلة على مستوى التطبيق مختلفة. إذا أرسل العميل أمرًا غير مدعوم، فالإطار نفسه صحيح من ناحية SWP؛ لذلك يرد الخادم برسالة ERROR تحمل Sequence الطلب، ويبقي الاتصال مفتوحًا.


#9. CRC32 والتحقق من سلامة البيانات

يستخدم SWP خوارزمية CRC-32 القياسية، وهي قيمة IEEE/zlib الشائعة نفسها:

text
CRC32("123456789") = 0xCBF43926

يشمل الحساب:

text
13-byte header + payload

ولا يشمل بايتات CRC32 الأربعة نفسها.

يعيد المستقبل حساب القيمة ويقارنها بما وصل في الإطار. فإذا تغير بايت في الترويسة أو الحمولة عرضيًا، يظهر عدم التطابق.

لكن يجب ألا نحمّل CRC32 وظيفة لا يملكها: هو ليس آلية أمنية. المهاجم الذي يستطيع تعديل الإطار يستطيع أيضًا حساب CRC جديد. لذلك لا يقدم CRC32 تشفيرًا أو مصادقة أو حماية من العبث المتعمد. عندما نحتاج هذه الخصائص، يأتي دور TLS.


#10. أنواع الرسائل ومعنى كل منها

يشغل نوع الرسالة بايتًا واحدًا في الترويسة. الأنواع الأساسية الأصلية تمتد من 0x01 إلى 0x09، بينما تستخدم الامتدادات القيم من 0x0A إلى 0x0D.

القيمةالاسمالاتجاهالحمولةالنتيجة
0x01HELLOClient → Serverتعريف العميل بصيغة UTF-8HELLO_ACK
0x02HELLO_ACKServer → Clientتعريف الخادم بصيغة UTF-8يؤكد بدء التحية
0x03TEXTClient → Serverنص UTF-8ACK، ويُبث أيضًا للعملاء الآخرين
0x04COMMANDClient → Serverأمر UTF-8 من قائمة السماحCOMMAND_RESULT أو ERROR
0x05COMMAND_RESULTServer → Clientمخرجات الأمر بصيغة UTF-8استجابة لـCOMMAND
0x06HEARTBEATClient → ServerفارغACK
0x07ACKServer → Clientفارغ عادةً؛ وقد يحمل نصًا عند اكتمال رفع ملفإقرار نجاح
0x08ERRORServer → Clientنص خطأ مقروء بصيغة UTF-8رفض طلب أو خطأ بروتوكول
0x09CLOSEClient → ServerفارغACK ثم إغلاق TCP
0x0AFILE_STARTClient → Serverالحجم، وSHA-256، واسم الملفACK أو ERROR
0x0BFILE_CHUNKClient → Serverبايتات الملف الخامACK أو ERROR
0x0CFILE_ENDClient → ServerفارغACK saved as ... أو ERROR
0x0DAUTHClient → Serverالمفتاح المشترك بصيغة UTF-8ACK أو ERROR

يمكن للخادم أيضًا إرسال TEXT من تلقاء نفسه بوصفه Chat Push. في هذه الحالة يكون Sequence 0 ويُضبط Broadcast Flag بالقيمة 0x01.

#10.1 ترميز النصوص

الحمولات النصية تستخدم UTF-8 من دون أي Terminator. حقل الطول وحده يحدد عدد البايتات التابعة للنص. وإذا احتوت الحمولة على UTF-8 غير صالح، فإن مساعد Packet المرجعي يعرض Replacement Characters بدل أن يتسبب في انهيار Decoder البروتوكول.

الاستثناء هو FILE_CHUNK: فهي بيانات ثنائية ولا ينبغي تفسيرها على أنها UTF-8.

#10.2 قائمة الأوامر المسموح بها

يدعم الخادم فقط الأوامر الآتية على مستوى التطبيق:

الأمرالمعنى
PINGيعيد PONG
GET_TIMEيعيد الوقت المحلي للخادم بصيغة ISO-8601 متضمنًا المنطقة الزمنية
GET_HOSTNAMEيعيد اسم المضيف للخادم
GET_STATUSيعيد مدة التشغيل وعدد العملاء النشطين
ECHO <text>يعيد النص المرسل

أسماء الأوامر غير حساسة لحالة الأحرف. وأي أمر آخر ينتج ERROR. يستدعي الخادم execute_command() مباشرة ولا يشغّل Shell نظام التشغيل مطلقًا. لذلك فإن طلبًا مثل:

text
rm -rf /

سيُعامل بوصفه أمرًا غير مدعوم، ولن يُنفذ.


#11. دورة حياة الاتصال وحالة البروتوكول

SWP بروتوكول Stateful؛ أي إن الخادم لا ينظر إلى كل رسالة بمعزل عما قبلها، بل يتتبع هل أرسل العميل التحية، وهل تمت المصادقة، وهل بدأ نقل ملف.

#11.1 تدفق جلسة أساسية

text
Client                                      Server
  |                                           |
  | ------- HELLO, seq=1 ------------------> |
  | <------ HELLO_ACK, seq=1 --------------- |
  |                                           |
  | ------- TEXT, seq=2 -------------------> |
  | <------ ACK, seq=2 --------------------- |
  |                                           |
  | ----- COMMAND "PING", seq=3 ----------> |
  | <---- COMMAND_RESULT "PONG", seq=3 ---- |
  |                                           |
  | ------- HEARTBEAT, seq=4 --------------> |
  | <------ ACK, seq=4 --------------------- |
  |                                           |
  | ------- CLOSE, seq=5 ------------------> |
  | <------ ACK, seq=5 --------------------- |
  |                                           |
  |              TCP connection closes       |

المتوقع أن يرسل العميل HELLO أولًا. وإذا وصل طلب آخر قبلها، يعيد الخادم ERROR بالنص HELLO required ويبقي الاتصال مفتوحًا.

#11.2 تدفق المصادقة الاختيارية

عندما يبدأ الخادم بمفتاح مصادقة، يصبح التسلسل:

text
Client                                      Server
  | ------- HELLO ------------------------> |
  | <------ HELLO_ACK --------------------- |
  | ------- AUTH shared-key --------------> |
  | <------ ACK ---------------------------- |
  | ------- normal requests --------------> |

إلى أن تنجح المصادقة، تستقبل الرسائل الطبيعية ERROR AUTH required. وإذا كان المفتاح خاطئًا، يعيد الخادم خطأ ثم يغلق الاتصال.

أما إذا لم يُضبط مفتاح أصلًا، فيعتبر الخادم الاتصال موثقًا مباشرة بعد تهيئته.

#11.3 الإغلاق

الإغلاق الطبيعي يبدأ على مستوى التطبيق قبل أن ينتهي على مستوى TCP:

  1. يرسل العميل CLOSE.
  2. يرد الخادم ACK بالـSequence نفسه.
  3. يضع الخادم حالة المعالج على Closing.
  4. يُغلق اتصال TCP.

إذا انقطع TCP فجأة، يعامل ذلك بوصفه Disconnect. وإذا كان رفع ملف جارٍ وقت الانقطاع، يحذف الخادم الملف المؤقت الجزئي أثناء عملية التنظيف.


#12. تنفيذ العميل

يوجد العميل المرجعي في client/client.py.

#12.1 الاتصال

ينشئ العميل اتصال TCP باستخدام socket.create_connection(). ويمكنه اختياريًا تغليف الـSocket المتصل بكائن ssl.SSLContext قبل بدء SWP. وبعد نجاح الاتصال، يشغّل Thread للقراءة في الخلفية.

#12.2 إرسال الطلبات

تقوم SWPClient.request_raw() بالآتي:

  1. تزيد عداد Sequence الخاص بالعميل.
  2. تنشئ كائن Packet.
  3. تسجل الحزمة الخارجة أو تعرضها إذا كان Verbose Mode مفعلًا.
  4. ترمزها عبر encode().
  5. ترسل جميع البايتات في الـSocket.
  6. تنتظر الرد التالي الذي ليس Broadcast.

وتجعل الدوال المساعدة نية التطبيق أوضح:

python
c.hello()
c.text("hello server")
c.command("PING")
c.heartbeat()
c.bye()

يرسل العميل المرجعي طلبًا واحدًا ثم ينتظر إجابته قبل إرسال الطلب التالي. هذا التصميم بسيط ومناسب لبروتوكول تعليمي، بينما يبقى Sequence ID موجودًا حتى يتمكن عميل أكثر تقدمًا مستقبلًا من دعم Pipelining.

#12.3 استقبال الردود وChat Push

يستمر Thread القراءة في استدعاء recv(65536) وتمرير ما يصل إلى FrameDecoder واحد.

  • إذا كان الإطار يحمل Broadcast Flag، يذهب إلى Callback باسم on_broadcast.
  • أي إطار آخر يدخل Reply Queue التي تستخدمها request_raw().
  • أي خطأ في Decoder أو Socket ينهي حلقة القراءة ويوقظ الطلب المنتظر.

هذه البنية ضرورية لأن رسالة محادثة قد تصل بينما ينتظر العميل ردًا على عملية أخرى.


#13. تنفيذ الخادم

يوجد الخادم في server/server.py، ويعتمد على socketserver.ThreadingTCPServer، بحيث يحصل كل عميل مقبول على Thread خاص به.

#13.1 بدء تشغيل الخادم

عند تشغيل الخادم من سطر الأوامر فإنه:

  1. يقرأ عنوان الربط والمنفذ وخيارات TLS والمصادقة والرفع.
  2. ينشئ اختياريًا TLS Server Context.
  3. ينشئ SWPServer.
  4. يربط Socket بمنفذ TCP المحدد ويبدأ الاستماع.
  5. يستدعي serve_forever().

المنفذ الافتراضي هو 9320، ويمكن تغييره. وفي الاختبارات يُستخدم المنفذ 0 كي يختار نظام التشغيل منفذًا متاحًا تلقائيًا.

#13.2 معالج كل عميل

يمتلك كل SWPHandler حالة مستقلة لاتصال واحد:

text
decoder       incremental FrameDecoder
greeted       whether HELLO was accepted
authed        whether authentication is complete
closing       whether CLOSE was received
xfer          current file-transfer state, if any
send_lock     protects concurrent writes

وتدور حلقة المعالج وفق المسار:

text
recv bytes
    |
    v
decoder.iter_feed(bytes)
    |
    v
dispatch(each complete Packet)
    |
    v
send response or update connection state

#13.3 لماذا يوجد Send Lock؟

قد يرسل Thread العميل نفسه استجابةً لطلب، وفي اللحظة ذاتها يحاول Thread تابع لعميل آخر إرسال Chat Broadcast إلى هذا العميل. ولو نُفذت عمليتا sendall() في وقت واحد من دون تنسيق، فقد تتداخل البايتات عند مستوى الكاتب التطبيقي.

لهذا يستخدم المعالج threading.Lock حول كل إطار مشفّر كامل. يحافظ هذا القفل على حدود الإطار عند الكتابة من التطبيق، بينما يضمن TCP ترتيب دفق البايتات في الطبقة الأدنى.

#13.4 ترتيب الـDispatch

يعالج الخادم الحزم المفكوكة تقريبًا بهذا الترتيب:

  1. يقبل HELLO ويرد بـHELLO_ACK.
  2. أي رسالة غير HELLO قبل التحية تستقبل HELLO required.
  3. يعالج AUTH عند وصولها.
  4. أي رسالة عادية قبل إتمام المصادقة المطلوبة تستقبل AUTH required.
  5. يبث TEXT ويرسل إقرارًا للمرسل.
  6. يقر HEARTBEAT.
  7. ينفذ COMMAND عبر قائمة السماح.
  8. تحدث رسائل الملفات حالة عملية الرفع الحالية.
  9. يقر CLOSE وينهي الحلقة.
  10. أي رسالة غير متوقعة تستقبل Error.

أما أخطاء صيغة البروتوكول فتعالج خارج الـDispatch الطبيعي. يرسل الخادم Error بـSequence صفر ثم يغلق الاتصال، لأنه لم يعد يستطيع الوثوق بقدرته على مواصلة فك الدفق بصورة صحيحة.


#14. امتداد بث المحادثة

كانت رسالة TEXT في التصميم الأصلي طلبًا من العميل إلى الخادم. أضاف الامتداد استخدامًا ثانيًا لها: يستطيع الخادم دفع رسالة TEXT إلى بقية العملاء المتصلين.

عندما يرسل العميل A:

text
TEXT, sequence=8, payload="hello everyone"

ينفذ الخادم عمليتين:

  1. يرسل ACK, sequence=8 إلى العميل A.
  2. يرسل Broadcast TEXT إلى كل عميل آخر أكمل التحية والمصادقة.

إطار البث يحمل:

text
sequence = 0
flags    = 0x01
payload  = "<sender-ip>:<sender-port>: hello everyone"

لا يستقبل المرسل Broadcast رسالته الخاصة. ويتحقق العميل من الـFlag ليوجه الإطار إلى Chat Callback بدل Reply Queue العادية.

وعندما يجهز الخادم قائمة المستلمين، يأخذ Snapshot للـHandlers المؤهلين بينما يحمل Client-list Lock، ثم ينفذ الإرسال بعد تحرير ذلك القفل. بهذه الطريقة لا يبقى القفل العام محتجزًا أثناء عمليات Network I/O.


#15. امتداد رفع الملفات

يعرض بروتوكول الملفات مجموعة من القضايا التي لا تظهر في الرسائل النصية البسيطة: Payloads ثنائية، وحالة تطبيق تمتد عبر عدة إطارات، وملفات مؤقتة، والتحقق من Hash، والتعامل الآمن مع نظام الملفات.

#15.1 تسلسل الرفع

text
Client                                      Server
  |                                           |
  | FILE_START: size sha256 filename ------> |
  | <------------------------------- ACK ---- |
  | FILE_CHUNK: bytes ---------------------> |
  | <------------------------------- ACK ---- |
  | FILE_CHUNK: bytes ---------------------> |
  | <------------------------------- ACK ---- |
  |              ...                         |
  | FILE_END ------------------------------> |
  | <------ ACK: "saved as name" ----------- |

يستخدم العميل المرجعي أسلوب Stop-and-wait: يرسل Chunk واحدًا ثم ينتظر ACK قبل إرسال التالي. التصميم واضح وسهل الاستدلال، ويجعل المرسل يعرف بالضبط أي Chunk قُبل، لكنه ليس التصميم الأعلى أداءً من ناحية Throughput.

#15.2 بيانات FILE_START

الحمولة النصية بصيغة UTF-8 هي:

text
<size> <sha256-hex> <filename>

مثال:

text
169219 0123456789abcdef... photo.jpg

وضع اسم الملف في الحقل الأخير يسمح له باحتواء مسافات. ويتحقق الخادم من أن:

  • الحجم رقم عشري.
  • قيمة SHA-256 مكونة من 64 خانة Hex صغيرة بالضبط.
  • الحجم لا يزيد على الحد المضبوط، وهو 25 MiB افتراضيًا.
  • اسم الملف يطابق نمط الأسماء الآمنة.

يقبل الخادم اسمًا يبدأ بحرف ASCII أو رقم، ثم يتبعه بحد أقصى 99 حرفًا من حروف ASCII أو الأرقام أو النقاط أو الشرطات السفلية أو المسافات أو الشرطات. بهذا تُرفض Path Separators، و..، والأسماء التي تبدأ بنقطة، ومحارف التحكم، والأسماء غير المحدودة الطول.

يقوم العميل أيضًا بأخذ Local Basename، واستبدال المحارف غير المدعومة، وإزالة البدايات غير الآمنة، وقصر الاسم على 100 محرف. لكن الخادم يعيد التحقق من الاسم النهائي؛ فالتنظيف على جهة العميل ليس حدًا أمنيًا.

#15.3 FILE_CHUNK

تحمل FILE_CHUNK بايتات خامًا. لا تُفك كنص، ولا يعرضها محلل Wireshark بوصفها نصًا.

يحدد البروتوكول حدًا أقصى لكل Chunk مقداره 64 KiB (MAX_FILE_CHUNK). يقرأ العميل المرجعي ويرسل Chunks لا تتجاوز هذا الحجم. أما الخادم فيفرض الحجم الإجمالي المعلن والحد العام لإطار SWP. ومن تحسينات Hardening المستقبلية الممكنة أن يرفض الخادم أيضًا أي Chunk منفرد يتجاوز حد 64 KiB صراحةً.

#15.4 التخزين والتحقق على جهة الخادم

ينفذ الخادم ما يأتي:

  1. ينشئ ملفًا مؤقتًا باسم .swp-*.part داخل مجلد الرفع المضبوط.
  2. يكتب كل Chunk مقبول في الملف.
  3. يحدّث SHA-256 بصورة Streaming.
  4. يرفض أي بايتات تتجاوز الحجم المعلن.
  5. عند FILE_END يتحقق من العدد النهائي للبايتات المستلمة.
  6. يقارن SHA-256 المحسوب بالقيمة المعلنة.
  7. ينشئ الاسم النهائي من دون الكتابة فوق ملف موجود.
  8. يتخلص من الاسم المؤقت بعد نجاح العملية.

إذا كان النقل ناقصًا أو أكبر من المسموح، أو احتوى بيانات زائدة، أو اسمًا غير صالح، أو Hash غير مطابق، يرسل الخادم ERROR ويحذف الملف المؤقت. وإذا انقطع الاتصال أثناء النقل، تنفذ عملية Cleanup الإلغاء نفسه.

إذا كان a.txt موجودًا مسبقًا، يحفظ الخادم الملف التالي باسم a-1.txt ثم a-2.txt وهكذا. وتستخدم عملية الوضع النهائي Hard Link ذرية حتى لا يتعمد الخادم الكتابة فوق وجهة موجودة.

يدعم البروتوكول رفع الملفات فقط؛ لا توجد حاليًا رسالة لتنزيل ملف.


#16. امتداد المصادقة

المصادقة اختيارية، وتُفعّل بتشغيل الخادم مع --auth-key أو عبر متغير البيئة SWP_KEY.

يرسل العميل بعد ذلك:

text
AUTH, payload=<shared key>

يقارن الخادم المفتاح باستخدام hmac.compare_digest(). إذا نجحت المقارنة، يعلّم الاتصال بوصفه مصادقًا عليه ويرسل ACK. وإذا فشلت، ينتظر مدة قصيرة ثم يرسل:

text
ERROR Authentication failed

ويغلق الاتصال. هذه المهلة تجعل التخمين السريع أقل سهولة، لكنها ليست نظام Rate Limiting متكاملًا.

المفتاح نفسه Payload داخل SWP. وفي وضع Plaintext يعبر الشبكة بنص صريح، ولهذا يحذر التنفيذ من استخدام --auth-key من دون TLS. الأفضل استخدام المفتاح مع TLS، ويفضل تمريره عبر SWP_KEY بدل وضعه في Command Line لأن وسائط سطر الأوامر قد تظهر في Process Listings.

هذه الآلية بوابة بسيطة تعتمد Shared Secret، وليست نظام هوية متكاملًا. لا توجد حسابات منفصلة للمستخدمين، ولا Key Rotation، ولا Challenge-response، ولا أدوار Authorization.


#17. وضع TLS

يمكن تشغيل SWP داخل TLS من دون أي تغيير في تنسيق إطار SWP:

text
Application data
        |
        v
SWP frame
        |
        v
TLS encryption and authentication
        |
        v
TCP connection

يستخدم التنفيذ في Python وحدة ssl من المكتبة القياسية، ويشترط TLS 1.2 أو أحدث. ويُفاوض على TLS 1.3 متى كان متاحًا.

يحمي TLS دفق SWP كاملًا، بما في ذلك:

  • أنواع الرسائل.
  • Flags.
  • أطوال الـPayload.
  • Sequence IDs.
  • النصوص والأوامر.
  • Metadata الملفات والـChunks.
  • قيم CRC الخاصة بـSWP.

يبقى CRC موجودًا داخل البروتوكول بوصفه فحص سلامة تعليميًا، لكن TLS يوفر أصلًا سلامة تشفيرية للاتصال المحمي.

#17.1 التحقق من الشهادة

يتحقق العميل الطبيعي من شهادة الخادم عبر ملف CA/Certificate المضبوط. ويعطل الخيار --insecure التحقق من الشهادة. يظل الاتصال مشفرًا في هذه الحالة، لكن مهاجم Man-in-the-Middle يستطيع انتحال الخادم، ولذلك فإن --insecure مخصص للاختبار فقط.

كل منفذ استماع يُضبط إما لـPlaintext SWP أو SWP مغلف بـTLS. لا يمكن مزج عملاء Plaintext وTLS على Socket الاستماع نفسه.

#17.2 تسجيل مفاتيح TLS من أجل Wireshark

لأغراض التعلم المحلي يمكن للطرفين كتابة أسرار TLS باستخدام --keylog. يستطيع Wireshark استخدام ملف Key Log المشترك لفك تشفير Capture ثم تمرير الدفق المفكوك إلى محلل SWP. أما الالتقاط الذي لا يملك Key Log فسيعرض TLS Application Data مشفرة فقط، وهذا هو السلوك المتوقع من ناحية الخصوصية.

ملفات Key Log تحتوي أسرارًا فعلية، ولذلك يجب عدم تمكينها أو مشاركتها في بيئات حقيقية.


#18. التعامل مع الأخطاء

يفرق SWP بوضوح بين أخطاء صيغة البروتوكول وبين طلبات سليمة من ناحية الـWire لكن التطبيق يرفض تنفيذها.

#18.1 أخطاء البروتوكول

هذه الحالات تعني أن الإطار نفسه لم يعد موثوقًا:

  • Magic غير صحيحة.
  • إصدار غير مدعوم.
  • نوع رسالة مجهول.
  • Payload أكبر من 1 MiB.
  • عدم تطابق CRC32.

يرفع الـDecoder استثناء بروتوكول محدد النوع مثل InvalidMagic أو UnsupportedVersion أو PayloadTooLarge أو ChecksumError. يبلغ الخادم عن الخطأ بإطار ERROR باستخدام Sequence 0 ثم يغلق الاتصال، لأن دفق البايتات قد يكون خرج عن التزامن.

#18.2 أخطاء التطبيق

أما هذه الحالات فهي طلبات SWP سليمة بنيويًا لكن لا يمكن تنفيذها:

  • لم تُرسل HELLO أولًا.
  • المصادقة مطلوبة.
  • الأمر ليس ضمن قائمة السماح.
  • نقل الملف في حالة غير صحيحة.
  • الملف أكبر من الحد.
  • اسم الملف غير صالح.
  • أُرسلت بايتات أكثر من الحجم المعلن.
  • قيمة SHA-256 لا تتطابق.

تستخدم هذه الأخطاء عادة Sequence ID الخاص بالطلب، ويمكن إبقاء الاتصال مفتوحًا متى بقيت حالة الخادم آمنة للاستمرار. أما فشل المصادقة فهو خطأ نهائي عمدًا وينتهي بإغلاق الاتصال.

#18.3 لماذا يُغلق الاتصال بعد إطار غير صالح؟

تخيل أن الترويسة تعلن Payload Length يساوي 100 بايت، لكن المرسل أرسل Layout مختلفًا. لو حاول المستقبل البحث عن البايتات SW التالية لإعادة التزامن، فقد تظهر SW مصادفة داخل Payload عشوائي. عندها قد يفسر المستقبل جزءًا من البيانات بوصفه إطارًا جديدًا وينفذ عملية لا يقصدها المرسل.

إغلاق الاتصال أكثر أمانًا وأبسط من محاولة إعادة مزامنة دفق TCP لم نعد نثق بحدوده.


#19. مثال كامل لحزمة واحدة

لنفترض أن العميل يريد إرسال الطلب الآتي:

text
Type:     COMMAND (0x04)
Sequence: 4
Payload:  GET_TIME
Flags:    0

طول الحمولة هنا 8 بايت. وتكون الترويسة:

text
53 57                magic = "SW"
01                   version = 1
04                   type = COMMAND
00                   flags = 0
00 00 00 08          payload length = 8
00 00 00 04          sequence = 4

أما الـPayload فهي:

text
47 45 54 5f 54 49 4d 45    "GET_TIME"

ويصبح الإطار الكامل كما يعرضه Packet-dump Tool:

text
53 57 01 04 00 00 00 00 08 00 00 00 04
47 45 54 5f 54 49 4d 45
0a b2 49 d6

البايتات الأربعة الأخيرة هي CRC32 المحسوبة على جميع البايتات السابقة داخل الإطار. يقرأ المستقبل أول 13 بايت، ويكتشف أن هناك 8 بايتات Payload، فينتظر حتى يصبح لديه 25 بايت كاملة، ثم يتحقق من الـChecksum.

أما الرد على الأمر فيكون إطار SWP جديدًا:

text
COMMAND_RESULT, sequence=4, payload="<current local ISO-8601 time>"

تطابق Sequence ID هو ما يخبر العميل بأن هذه الاستجابة تخص طلب GET_TIME.


#20. كيف بُني المشروع خطوةً خطوة؟

بُني التنفيذ على طبقات، بحيث يمكن عزل كل مفهوم شبكي واختباره قبل الانتقال إلى ما بعده.

#الخطوة 1: تعريف الثوابت وبنية الـWire

كانت القرارات الأولى هي Magic Bytes، والإصدار، والمنفذ، وحقول الترويسة، وترتيب البايتات، والحد الأعلى للحمولة، وقيم أنواع الرسائل. وضع هذه القيم في protocol/constants.py منع تكرار الأرقام في العميل والخادم.

#الخطوة 2: إنشاء نموذج بيانات للحزمة

يمثل Packet رسالة منطقية واحدة بصورة مستقلة عن الـSocket. فهو يحفظ نوع الرسالة، وSequence، والـPayload، والـFlags، والإصدار. بذلك تحصل بقية أجزاء المشروع على كائن واضح يمكن تمريره واختباره.

#الخطوة 3: تنفيذ الـEncoder

حوّل الـEncoder كائن Packet إلى Bytes. وتحققت اختبارات Exact-layout من أن:

  • Magic وVersion يظهران في المواضع المتوقعة.
  • الأطوال وSequence IDs تستخدم Big-endian.
  • Payload فارغة لا تزال تنتج إطارًا حجمه 17 بايت.
  • CRC محسوب على البايتات الصحيحة.

#الخطوة 4: تنفيذ Decoder تدريجي

بُني Decoder حول مشكلة دفق TCP نفسها، لا حول افتراض أن كل قراءة تساوي رسالة. لذلك تغذيه الاختبارات بإطار بايتًا بعد بايت، وتجمع إطارين في Buffer واحد، وتقسم الإطار الثاني على عدة مدخلات.

#الخطوة 5: بناء خادم Request/Response

بعد ذلك رُبط الخادم بمستمع TCP متعدد الخيوط. استخدم Decoder للمدخلات وEncoder للمخرجات، مع Dispatch Method تربط أنواع الرسائل بالسلوك المناسب.

#الخطوة 6: بناء العميل

أضاف العميل تخصيص Sequence IDs، ودوال الطلب المساعدة، وReply Queues، وقارئًا يعمل في الخلفية، وواجهة تفاعلية، وDemo Script. ويجعل عرض TX/RX العلاقة بين فعل التطبيق والإطار على الشبكة مرئية مباشرة.

#الخطوة 7: إضافة امتدادات آمنة

توسع البروتوكول من دون تغيير الترويسة:

  • أعادت المحادثة استخدام TEXT مع Broadcast Flag.
  • استخدم رفع الملفات ثلاثة أنواع رسائل جديدة وحالة نقل صريحة.
  • استخدمت المصادقة AUTH قبل الطلبات الطبيعية.
  • غلف TLS الـSocket تحت SWP.

#الخطوة 8: إضافة قابلية الرصد

تعرض أداة Packet-dump قيم الحقول والبايتات الخام. ويتعرف Lua Dissector الخاص بـWireshark إلى SWP على TCP Port 9320، ويطلب مزيدًا من البايتات عندما يكون الإطار منقسمًا، ويدور على عدة إطارات إذا وُجدت داخل TCP Segment واحدة.

#الخطوة 9: اختبار الحدود والحالات الصعبة

لا تغطي الاختبارات الرسائل الناجحة فقط، بل الحالات التي تميل إلى كسر البروتوكول فعلًا:

  • Magic خاطئة.
  • إصدار غير مدعوم.
  • نوع مجهول.
  • CRC غير صحيح.
  • Payload ضخمة معلنة في الترويسة فقط.
  • إدخال مجزأ.
  • إدخال مدمج.
  • رفض أمر.
  • طلب يصل قبل HELLO.
  • التحقق من حجم الملف واسمه.
  • عدم تطابق Checksum وتنظيف الرفع الجزئي.
  • نجاح المصادقة وفشلها.
  • توجيه رسائل المحادثة.
  • التحقق من TLS والفصل بين Plaintext وTLS.

#21. تشغيل المشروع وعرضه عمليًا

شغّل الأوامر التالية من جذر المستودع.

#21.1 تشغيل خادم Plaintext

bash
python -m server.server --host 0.0.0.0 --port 9320

#21.2 تشغيل العميل التفاعلي

bash
python -m client.client --host 127.0.0.1 --port 9320

يدعم الـPrompt:

text
text hello server
command PING
command GET_STATUS
command ECHO hello
heartbeat
quit

#21.3 تشغيل الـDemo الجاهز

bash
python -m client.client --host 127.0.0.1 --port 9320 --demo

يختبر الـDemo التحية، والنص، والأوامر، وHeartbeat، والإغلاق، مع عرض Sequence IDs.

#21.4 فحص إطار من دون خادم

bash
python tools/packet_dump.py --type COMMAND --seq 4 --payload GET_TIME

ولفك إطار Hex موجود مسبقًا:

bash
python tools/packet_dump.py --hex \
  "53 57 01 04 00 00 00 00 08 00 00 00 04 47 45 54 5f 54 49 4d 45 0a b2 49 d6"

#21.5 رفع ملف

ابدأ خادمًا مع TLS ومفتاح مصادقة اختياري:

bash
tools/gen_cert.sh
python -m server.server --host 0.0.0.0 --port 9320 \
  --tls --auth-key mysecret

ثم ارفع الملف من العميل:

bash
python -m client.client --host 127.0.0.1 --port 9320 \
  --tls --cafile certs/server.crt --key mysecret \
  --send-file ./photo.jpg

يحفظ الخادم الملفات المقبولة داخل received/ ما لم يُحدد --upload-dir مختلف.

#21.6 تجربة المحادثة

افتح عميلين متصلين بالخادم نفسه. ستُقر رسالة text ... عند المرسل، بينما تظهر بوصفها Broadcast في Prompt العميل الآخر.

#21.7 تشغيل الاختبارات

bash
python -m unittest discover -s tests -t . -v

لا يحتاج المشروع في وقت التشغيل إلى أي Dependency خارج مكتبة Python القياسية.


#22. Wireshark وقابلية الرصد

يسجل الـDissector الموجود في wireshark/swp.lua بروتوكول SWP على TCP Port 9320. ويعرض حقولًا مثل:

text
swp.magic
swp.version
swp.type
swp.flags
swp.length
swp.sequence
swp.payload
swp.crc32

ويكشف الـDissector درسًا مهمًا آخر في التعامل مع Streams؛ فـTCP Segment واحدة قد تحتوي:

  • جزءًا فقط من ترويسة SWP.
  • إطارًا كاملًا.
  • إطارًا كاملًا مع جزء من إطار تالٍ.
  • عدة إطارات كاملة.

يستخدم Lua Code حقول TCP Desegmentation في Wireshark لطلب البايتات الناقصة، ثم يكرر التحليل إذا كان Segment يحتوي أكثر من إطار. وهو لا يعيد حساب CRC32 عمدًا؛ فـPython Decoder واختبارات صيغة الـWire تتوليان التحقق من البروتوكول، بينما يركز Wireshark على جعل الحقول مرئية.

من Display Filters المفيدة:

text
tcp.port == 9320
swp
swp.type == 4
swp.sequence == 3
swp.version == 1
swp.length > 10
swp.payload contains "PING"

في وضع TLS يظهر Capture الطبيعي بوصفه TLS Application Data مشفرة. ولرؤية إطارات SWP بعد فك التشفير، زود Wireshark بملف Debug Key-log، ثم -عند الحاجة- عرّف المنفذ المفكوك على أنه SWP.


#23. أين يمكن الاستفادة من البروتوكول؟

قيمة SWP تظهر خصوصًا في البيئات التعليمية والمختبرات المضبوطة، لأنه يجعل التفاصيل التي تختفي عادةً خلف المكتبات جاهزة أمامك.

#23.1 تعلم برمجة الشبكات

يعرض Sockets والمنافذ وTCP Streams والقراءة Blocking ودورة حياة الاتصال وتزامن الخادم، داخل Codebase صغيرة نسبيًا.

#23.2 تعلم تصميم البروتوكولات

يوضح كيف يعرّف البروتوكول حقولًا، وأنواع رسائل، وحدود أطوال، وانتقالات حالة، وأخطاء، وSequence IDs، ونقاط توسعة.

#23.3 المراقبة المحلية وفحوصات الحالة

يمكن للأوامر الموجودة في قائمة السماح تقديم واجهة حالة صغيرة ومضبوطة لجهاز مختبر موثوق. أوامر PING وGET_TIME وGET_HOSTNAME وGET_STATUS أمثلة محدودة عمدًا.

#23.4 اتصال نصي مضبوط

يعرض امتداد المحادثة Server Fan-out، والرسائل غير المطلوبة، واستخدام Flags، والحاجة إلى فصل Push Messages عن ردود الطلبات.

#23.5 إرسال الصور أو الملفات داخل المختبر

يوضح امتداد الرفع كيف يمكن تقسيم Binary Transfer إلى إطارات، وإقرار كل جزء، وحساب Hash، والتحقق من البيانات، ثم حفظ الملف بأمان.

#23.6 تحليل الحزم والعروض التعليمية

تجعل أداة Packet-dump وSample Capture والـLogging وWireshark Dissector البروتوكول مناسبًا للعروض الصفية والتقارير والتجارب.


#24. النموذج الأمني والقيود

الوضع الافتراضي لـSWP هو Plaintext ومن دون مصادقة. أي جهة تستطيع مراقبة الشبكة قد تقرأ الرسائل، وأي جهة تستطيع الاتصال يمكنها محاولة إرسال الطلبات. عند التعامل مع بيانات حساسة يجب استخدام TLS مع التحقق من الشهادة.

ومن القيود المهمة:

  • CRC32 يكشف التلف العرضي لكنه لا يقاوم العبث المتعمد.
  • المفتاح المشترك يُرسل داخل Payload ولا ينبغي استخدامه من دون TLS.
  • المفتاح المشترك لا يميز مستخدمين أفرادًا ولا يحدد صلاحيات مختلفة.
  • الخيار --insecure يعطل التحقق من شهادة الخادم ويسمح بانتحاله.
  • نموذج Thread-per-client لا يملك حدود اتصالات أو Rate Limits إنتاجية.
  • لا يوجد تصميم مستقل للحماية من Replay يتجاوز Convention الـSequence داخل الجلسة.
  • يستخدم العميل المرجعي Stop-and-wait في رفع الملفات، مفضلًا البساطة على Throughput.
  • لا توجد عملية لتنزيل الملفات.
  • مجموعة أوامر الخادم صغيرة عمدًا وليست Remote Administration API عامة.
  • التنفيذ الافتراضي مخصص للتعلم والاختبار والشبكات المضبوطة.

أكثر إعداد عملي أمانًا للعرض التجريبي هو:

text
TLS enabled
server certificate verified
shared key sent through an environment variable
small upload limit
restricted upload directory
allowlisted commands only

أما في نظام إنتاجي، فمن الأفضل استخدام بروتوكول ومكتبة أمنية ناضجين وخضعا للمراجعة، بدل نشر هذا التنفيذ التعليمي مباشرة.


#25. ماذا يعلّمنا هذا المشروع؟

أهم ما يتركه SWP ليس اسم بروتوكول جديد بقدر ما يترك طريقة مختلفة للنظر إلى الاتصال الشبكي نفسه:

  1. الـSocket ليس Message Queue. يمنحنا TCP دفق بايتات مرتبًا، ولذلك يجب أن يحدد التطبيق حدود رسائله بنفسه.
  2. حقل الطول يجب ألا يُوثق به بلا تحقق. ينبغي التحقق منه قبل Buffering لتجنب استنزاف الذاكرة، ثم انتظار الحجم المعلن بالضبط.
  3. الترميز وفك الترميز عقد واحد بطرفين. لا يصبح Packet Format مفيدًا إلا إذا اتفق الطرفان على Byte Order، ومواضع الحقول، والحدود، والـChecksums.
  4. حالة البروتوكول مهمة. تعطي HELLO وAUTH الاختيارية ومراحل نقل الملفات وCLOSE معنى لبايتات قد تبدو صحيحة بمفردها.
  5. Checksum ليس Encryption. سلامة البيانات من التلف العرضي شيء، والسرية والمصادقة التشفيريتان شيء آخر.
  6. الرسائل غير المطلوبة تحتاج قاعدة توجيه. يفصل Broadcast Flag مع Sequence 0 Chat Pushes عن الردود الطبيعية.
  7. قابلية الرصد تحسن الفهم. Hex Dump وLogs والاختبارات وWireshark تحول دفق البايتات غير المرئي إلى شيء يمكن مشاهدته وفحصه.
  8. تصميم الخصائص بأمان مهم حتى في مشروع تعليمي. قائمة سماح للأوامر، والتحقق من اسم الملف، وحدود الحجم، والملفات المؤقتة، ومنع الكتابة فوق الملفات؛ كلها تمنع المختبر التعليمي من التحول إلى Shell أو File Service غير آمن.

#26. الخلاصة

أنشأت SWP بوصفه مثالًا كاملًا ومقروءًا على كيفية بناء بروتوكول تطبيقي فوق TCP. يبدأ كل شيء من Binary Frame محدد بعناية؛ يستخدم حقل Length لحل مشكلة حدود الرسائل داخل TCP Stream، ويستخدم CRC32 لاكتشاف التلف العرضي، ويستخدم Sequence IDs لربط الطلبات بالاستجابات. ثم يأتي Client وخادم متعدد الخيوط ليجعلا البروتوكول قابلًا للتشغيل، وتأتي الاختبارات وWireshark لتجعلاه قابلًا للتحقق والمشاهدة.

يمكن اختصار الفكرة كلها في المسار:

text
define bytes -> send bytes over TCP -> rebuild frames -> validate -> interpret message

ثم تكشف امتدادات المحادثة والملفات والمصادقة وTLS وأدوات الفحص كيف يمكن لبروتوكول صغير أن ينمو من دون أن يفقد عقده الأساسي أو يصبح غامضًا. وهذه الموازنة -أن يكون بسيطًا بما يكفي للدراسة، وكاملًا بما يكفي لمواجهة مسائل الشبكات الحقيقية- هي السبب الذي بُني من أجله Sawlah Wire Protocol.


المواصفة التقنية لبروتوكول Sawlah Wire Protocol (SWP) — الإصدار 1.0

الحالة: مواصفة تعليمية، وليست مخصصة للاستخدام الإنتاجي.

#1. المقدمة

SWP بروتوكول تطبيقي ثنائي صغير يعتمد Request/Response ويستخدم Length-prefix لتحديد حدود الرسائل. صُمم لتعليم مفاهيم Framing، والترميز وفك الترميز، وإعادة تجميع Streams، والتحقق من السلامة، وتحليل البروتوكول داخل Wireshark.

#2. المصطلحات

  • Frame / Packet: رسالة SWP واحدة كما تظهر على الشبكة.
  • Client: الطرف الذي يفتح اتصال TCP ويرسل الطلبات.
  • Server: الطرف الذي يقبل الاتصال ويرد على كل طلب.
  • تستخدم الكلمات MUST وSHOULD وMAY بالمعنى المعرّف في RFC 2119؛ أي إن الأولى تعبّر عن متطلب إلزامي، والثانية عن توصية معيارية، والثالثة عن سلوك اختياري.

#3. طبقة النقل

يعمل SWP v1 مباشرة فوق TCP. وبما أن TCP دفق بايتات، فإن الشبكة لا تحافظ على حدود الإطارات. لذلك يجب على المستقبل MUST تخزين البيانات في Buffer وإعادة تجميع الإطارات باستخدام حقل Payload Length. قد يعيد نداء recv() جزءًا من إطار، أو إطارًا كاملًا، أو عدة إطارات دفعة واحدة.

#4. منفذ TCP

منفذ الخادم الافتراضي هو 9320. ويجب MUST على أي تنفيذ السماح بتغييره.

#5. ترتيب البايتات

جميع الأعداد متعددة البايتات تستخدم big-endian، أي Network Byte Order.

#6. بنية الحزمة

text
 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+---------------+---------------+---------------+---------------+
|   Magic 'S'   |   Magic 'W'   |    Version    | Message Type  |
+---------------+---------------+---------------+---------------+
|     Flags     |              Payload Length (4 bytes)         ...
+---------------+---------------+---------------+---------------+
...              |              Sequence ID (4 bytes)          ...
+---------------+---------------+---------------+---------------+
...              |         Payload (Payload Length bytes)      ...
+---------------+---------------+---------------+---------------+
|                          CRC32 (4 bytes)                      |
+---------------+---------------+---------------+---------------+

الإزاحات هي: Magic من 0 إلى 1، وVersion عند 2، وType عند 3، وFlags عند 4، وLength من 5 إلى 8، وSequence من 9 إلى 12، ثم Payload من 13 إلى 13+N-1، وأخيرًا CRC32 من 13+N إلى 16+N.

حجم الترويسة 13 بايت، وأصغر إطار حجمه 17 بايت عندما تكون الحمولة فارغة.

#7. حقول الترويسة

الحقلالحجمالوصف
Magic2ASCII SW = 53 57.
Version1القيمة 0x01. يجب MUST رفض أي إطار يحمل قيمة أخرى.
Message Type1راجع القسم 8. يجب MUST رفض القيم المجهولة.
Flags1محجوزة؛ يجب MUST على المرسل إرسال 0، ويتجاهلها المستقبل في المواصفة الأساسية.
Payload Length4عدد غير موقّع، بحد أقصى 1,048,576 بايت (1 MiB). يجب MUST رفض القيم الأكبر فور قراءة الترويسة وقبل Buffering الحمولة.
Sequence ID4عدد غير موقّع؛ راجع القسم 10.

#8. أنواع الرسائل

القيمةالاسمالاتجاهالحمولة
0x01HELLOC→Sتعريف العميل، مثل SWP Client 1.0
0x02HELLO_ACKS→Cتعريف الخادم، مثل SWP Server 1.0
0x03TEXTC→Sنص حر
0x04COMMANDC→Sأمر من قائمة السماح الموضحة أدناه
0x05COMMAND_RESULTS→Cمخرجات الأمر
0x06HEARTBEATC→Sفارغ
0x07ACKS→Cفارغ
0x08ERRORS→Cنص خطأ مقروء للبشر
0x09CLOSEC→Sفارغ
0x0A–0x0DFILE_START, FILE_CHUNK, FILE_END, AUTHC→Sراجع القسم 19

الأوامر — وهي قائمة سماح على مستوى التطبيق ولا تُمرر مطلقًا إلى Shell نظام التشغيل:

  • PINGPONG
  • GET_TIME → الوقت المحلي بصيغة ISO-8601
  • GET_HOSTNAME → اسم مضيف الخادم
  • GET_STATUSOK uptime=<s>s clients=<n>
  • ECHO <text><text>

أسماء الأوامر غير حساسة لحالة الأحرف. أي قيمة أخرى تنتج رد ERROR.

#9. إنشاء الاتصال

text
Client                          Server
  | -------- HELLO ------------> |
  | <----- HELLO_ACK ----------- |
  | -------- TEXT -------------> |
  | <--------- ACK ------------- |
  | ------- COMMAND -----------> |
  | <--- COMMAND_RESULT -------- |
  | ------ HEARTBEAT ----------> |
  | <--------- ACK ------------- |
  | -------- CLOSE ------------> |
  | <--------- ACK ------------- |

يجب MUST على العميل إرسال HELLO أولًا. وإذا استقبل الخادم طلبًا آخر قبل HELLO، فإنه يرد برسالة ERROR ويبقي الاتصال مفتوحًا.

#10. معرّفات التسلسل Sequence IDs

يبدأ العميل بالقيمة 1 ويزيدها بمقدار واحد لكل طلب. ينسخ الخادم Sequence ID الخاص بالطلب في الاستجابة، بحيث يمكن ربط الرد بطلبه. أما إطارات ERROR الناتجة عن مخالفة بروتوكول لا يمكن ربطها بطلب بعينه فتستخدم Sequence ID بالقيمة 0.

#11. ترميز الحمولة

الحمولات النصية تستخدم UTF-8 من دون Terminator، لأن حقل الطول هو الذي يحدد حدودها. وإذا احتوت الحمولة على UTF-8 غير صالح، تُعرض Replacement Characters ولا يُعد ذلك بحد ذاته Protocol Error.

#12. CRC32

يُستخدم CRC-32 وفق IEEE 802.3 / zlib، مع Polynomial المنعكس 0xEDB88320، وقيمة ابتدائية 0xFFFFFFFF، وFinal XOR بالقيمة 0xFFFFFFFF.

يُحسب CRC على البايتات من 0 حتى 12+N: أي الترويسة ذات 13 بايت متبوعة بالحمولة. حقل CRC نفسه غير مشمول بالحساب، وتُخزّن النتيجة Big-endian في آخر أربعة بايتات من الإطار.

قيمة التحقق المرجعية:

text
CRC32("123456789") = 0xCBF43926

#13. التعامل مع الأخطاء

يتحقق المستقبل، بالترتيب، من: Magic، ثم Version، ثم Message Type، ثم أن Payload Length ≤ 1 MiB اعتمادًا على الترويسة وحدها، وبعد وصول الإطار كاملًا يتحقق من CRC32.

عند أي مخالفة، ينبغي SHOULD للمستقبل إرسال إطار ERROR يذكر المشكلة، مثل:

text
ChecksumError: CRC32 mismatch ...

ثم يجب MUST إغلاق الاتصال، لأن دفق البايتات لم يعد موثوقًا بأنه مصطف على حدود الإطارات الصحيحة. أما الأمر المجهول فهو مختلف: ينتج ERROR لكن يبقى الاتصال مفتوحًا.

#14. إغلاق الاتصال

يرسل العميل CLOSE، ويرد الخادم بـACK يحمل Sequence ID نفسه، ثم يغلق اتصال TCP. وإذا أغلق أحد الطرفين TCP بصورة مفاجئة، يعامل ذلك على أنه Disconnect وتُنفذ عملية التنظيف من دون اعتباره خطأ بروتوكول إضافيًا.

#15. الاعتبارات الأمنية

SWP v1 في ذاته Plaintext؛ فهو لا يعرّف تشفيرًا أو مصادقة. ويجوز MAY للتنفيذ حمل SWP داخل TLS 1.2+ على المنفذ نفسه، كما يفعل التنفيذ المرجعي باستخدام --tls. لا يتغير Frame Format في هذه الحالة، لكن المنفذ الواحد يقدم إما Plaintext وإما TLS، وليس الاثنين معًا.

عند استخدام TLS مع التحقق من شهادة الخادم، يوفر TLS السرية ومصادقة الخادم. أما CRC32 فلا يكشف إلا التلف العرضي ولا يقدم حماية من مهاجم يتعمد تعديل البيانات. لذلك لا ينبغي إرسال بيانات حساسة في الوضع غير المشفر.

COMMAND قائمة سماح ثابتة، وعمليات رفع الملفات محصورة في مجلد واحد كما يوضح القسم 19؛ ويجب MUST NOT على التنفيذ تشغيل أوامر Shell عشوائية. كما تُفرض حدود الطول قبل تخصيص الذاكرة لمقاومة محاولات استنزافها. وأي استخدام إنتاجي حقيقي سيتطلب TLS ومصادقة مناسبة.

#16. مثال على حزمة

رسالة TEXT بالـSequence 1 والحمولة hello:

text
Magic: SW   Version: 1   Type: TEXT (0x03)   Flags: 0
Length: 5   Sequence: 1  Payload: "hello"    CRC32: over the 18 preceding bytes

#17. مثال Hexadecimal

رسالة COMMAND بالـSequence 4 والحمولة GET_TIME، وحجم الإطار 25 بايت كما تنتجها tools/packet_dump.py:

text
53 57 | 01 | 04 | 00 | 00 00 00 08 | 00 00 00 04 | 47 45 54 5f 54 49 4d 45 | 0a b2 49 d6
magic  ver  type flags   length=8      seq=4         "GET_TIME"                CRC32

#18. اكتشاف البروتوكول في Wireshark

يسجل wireshark/swp.lua الـDissector على:

text
tcp.port == 9320

يتحقق من البايتات 53 57 في بداية كل TCP Payload، ويستخدم pinfo.desegment_len وpinfo.desegment_offset لطلب مزيد من البيانات عندما يكون الإطار منقسمًا، ويكرر التحليل لاستخراج عدة إطارات من Segment واحدة، ويضبط عمود Protocol على SWP.

الحقول التي يعرضها:

text
swp.magic
swp.version
swp.type
swp.flags
swp.length
swp.sequence
swp.payload
swp.crc32

لا يعيد الـDissector حساب CRC.

#19. امتدادات v1.1 — مع بقاء Wire Version عند 1

أضيفت أنواع الرسائل التالية، وكلها من Client إلى Server ما لم يُذكر غير ذلك. يرد الخادم على كل طلب بـACK أو ERROR يحمل Sequence ID الخاص بالطلب:

القيمةالاسمالحمولة
0x0AFILE_STARTUTF-8 بالشكل <size> <sha256-hex> <filename>؛ اسم الملف هو الحقل الأخير وقد يحتوي مسافات
0x0BFILE_CHUNKبايتات خام، بحد أقصى 65,536 بايت لكل Chunk، وليست نصًا
0x0CFILE_ENDفارغ؛ وعند النجاح يكون الرد ACK بحمولة saved as <name>
0x0DAUTHمفتاح مشترك UTF-8؛ النجاح ACK، والفشل ERROR ثم يغلق الخادم الاتصال

#إجراء رفع الملف

يكون التسلسل:

text
FILE_START -> FILE_CHUNK(s) -> FILE_END

مع طلب واحد فقط قيد الانتظار في كل لحظة، وفق Stop-and-wait، ونقل واحد لكل اتصال.

يجب MUST على المستقبل:

  • رفض الأحجام التي تتجاوز الحد المضبوط عند FILE_START، والحد الافتراضي 25 MiB.
  • قبول أسماء الملفات المطابقة فقط للنمط:
text
[A-Za-z0-9][A-Za-z0-9._ -]{0,99}

وبذلك لا يُسمح بـ/ أو .. أو الاسم الذي يبدأ بنقطة.

  • رفض أي بايتات تتجاوز الحجم المعلن.
  • التحقق عند FILE_END من الحجم النهائي وSHA-256، وحذف الملف عند عدم التطابق أو انقطاع الاتصال.
  • تخزين الملفات في مجلد ثابت واحد.
  • عدم الكتابة فوق ملف موجود؛ بل إضافة Numeric Suffix إلى الاسم.

لا توجد عملية Download.

#المحادثة

عند استلام TEXT، يرسل الخادم ACK للمرسل، ثم يدفع النص إلى كل عميل آخر جاهز على هيئة إطار TEXT بـSequence ID 0، مع ضبط البت 0x01 في Flags بوصفه Broadcast، وبحمولة:

text
<sender>: <text>

يجب MUST NOT على العملاء اعتبار الإطارات التي تحمل Broadcast Flag ردودًا على طلباتهم.

#المصادقة AUTH

إذا كان الخادم مضبوطًا بمفتاح، فإن كل رسالة غير HELLO وAUTH تستقبل:

text
ERROR AUTH required

إلى أن تنجح AUTH. يُرسل المفتاح بنص صريح داخل SWP، ولذلك يجب MUST استخدامه عبر TLS فقط. يقارن الخادم المفتاح بزمن ثابت constant time.