مقاله فنی

بک‌اند timestamp یک libcurl برای PDFium VCL روی FPC

PDFium VCL درخواست‌های timestamp یک RFC 3161 را روی هدف‌های غیرویندوزی از طریق libcurl می‌فرستد، باندشده به‌شکل داینامیک به هشت سیمبول، آینه‌کردن شکل بک‌اند ویندوزی که به WinHTTP باند می‌شود. دو تنظیم گزینه تعیین می‌کنند حمل‌ونقل زیر لود قابل‌اعتماد است یا نه، و کل یونیت روی ماشینی اعتبارسنجی شد که نمی‌توانست آن را برای پلتفرم هدفش کامپایل کند

timestamping همان چیزی است که یک امضا را به چیزی تبدیل می‌کند که از انقضای گواهی جان سالم به در می‌برد، و یک عملیات شبکه است که درون یک عملیات امضا نشسته. همین ترکیب است که انتخاب transport را به‌شکلی که معمولاً نیست سرنوشت‌ساز می‌کند: روی یک worker thread اجرا می‌شود، با سروری حرف می‌زند که کنترلش دست شما نیست، و یک هنگ در آنجا یک خط لوله امضا را نگه می‌دارد نه یک لود صفحه

چرا libcurl به‌جای HTTP client خود FPC؟

چون گزینه دیگر یک پشته TLS را به مخزن کد می‌کشد و بعد تشخیص نسخه‌اش را به عهده شما می‌گذارد. مسیر بدیهی روی Free Pascal یکی fphttpclient با لایه سوکت OpenSSL است، و روی جزئیات شکست می‌خورد: بایندینگ‌های OpenSSL فری پاسکال 3.2.2 تشخیص OpenSSL 3.x را روی بیشتر توزیع‌های فعلی غیرقابل‌اعتماد انجام می‌دهند، و macOS هم تفاوت‌های LibreSSL را رویش اضافه می‌کند. چیزی که با یک فراخوانی HTTP کوچک شروع می‌شود به نگهداری دائمی ABI یک TLS دیگران تبدیل می‌شود

libcurl بک‌اند TLS خودش را resolve می‌کند و زنجیره‌ها را در برابر trust store پلتفرم اعتبارسنجی می‌کند، پس سمت پاسکال به هیچ‌کدام نیاز ندارد. لایه بایندینگ هشت سیمبول است. همین عدد استدلال است: سطح کوچک‌تر بین کد شما و یک وابسته متحرک یعنی جای کمتری برای اینکه یک ارتقای توزیع شما را بشکند، و با بک‌اند ویندوزی موجود جور است که همان‌جوری مشتی نقطه ورود WinHTTP را باند می‌کند

uses
  FPdfTsaFpc;

var
  ReqDer, RespDer: TBytes;
begin
  if not TsaHttpAvailable then
    raise Exception.Create('no HTTP transport for timestamping');

  Writeln('TSA transport: ', TsaHttpBackendName);

  ReqDer := BuildTimeStampQuery(DocumentDigest);
  if PostTimeStampQuery('https://tsa.example.org/tsr', ReqDer, RespDer) then
    AttachTimeStampToken(RespDer)
  else
    raise Exception.Create('timestamp request failed');
end;

اعلان تابع variadic زبان C در پاسکال

curl_easy_setopt و curl_easy_getinfo در سمت C variadic هستند، و Object Pascal راهی برای بیان آن ندارد. رویکردی که کار می‌کند اعلان چند prototype ثابت است، یکی به‌ازای هر کلاس آرگومان، که همه به همان سیمبول اکسپورت‌شده اشاره می‌کنند: یک واریانت گیرنده long، یک واریانت گیرنده pointer و به همین ترتیب، که در محل فراخوانی بر اساس چیزی که واقعاً پاس می‌دهید انتخاب می‌شوند

این برای دلیلی مشخص امن است که ارزش فهمیدن دارد نه کپی‌کردن. هر کدام از آن تایپ‌های آرگومان زیر قراردادهای فراخوانی پلتفرمِ در جریان در یک رجیستر عدد صحیح پاس می‌شود، که دقیقاً همان جایی است که پیاده‌سازی C با va_arg آن را می‌خواند. پس این ترفند برای عدد صحیح، pointer و handle برقرار است، و برای آرگومان‌های ممیز شناور که در رجیسترهای متفاوتی سفر می‌کنند برقرار نیست. با این فرض که الگو تعمیم می‌یابد یک واریانت گیرنده double اضافه نکنید

// یک سیمبول اکسپورت‌شده، چند prototype ثابت. هر واریانت آرگومانش را در
// یک رجیستر عدد صحیح پاس می‌دهد، که همان جایی است که سمت C می‌خواندش.
// واریانت ممیز شناور کار نمی‌کند و نباید اضافه شود
type
  TCurlSetOptLong = function(Handle: Pointer; Option: Integer;
    Value: NativeInt): Integer; cdecl;
  TCurlSetOptPtr  = function(Handle: Pointer; Option: Integer;
    Value: Pointer): Integer; cdecl;

var
  curl_easy_setopt_long: TCurlSetOptLong;
  curl_easy_setopt_ptr:  TCurlSetOptPtr;

دو تنظیمی که تعیین می‌کنند درخواست کامل می‌شود یا نه

اولی یک هدر خالی Expect: صریح است. libcurl هندشیک 100-continue یک HTTP را برای بدنه درخواست‌های بالای حدود یک کیلوبایت روشن می‌کند، و یک query timestamp با درخواست گواهی معمولاً از آن آستانه رد می‌شود. بعضی سرورهای TSA هرگز به continuation جواب نمی‌دهند، پس کلاینت یک timeout کامل را تمام می‌کند قبل از فرستادن بدنه‌ای که سرور فوراً قبول می‌کرد. فرستادن یک هدر خالی Expect: هندشیک را خاموش می‌کند، و درخواست در یک رفت‌وبرگشت از راه می‌رسد

دومی CURLOPT_NOSIGNAL است که باید ست شود. بدون آن libcurl timeout مربوط به resolve کردن نام را با SIGALRM پیاده می‌کند، و آن مکانیزم thread-safe نیست. امضا روی یک worker thread اجرا می‌شود، پس رفتار پیش‌فرض یک کرش نهفته است که زیر هم‌زمانی ظاهر می‌شود و در تست تک‌نخ هرگز. ست‌کردن پرچم مسیر مبتنی بر سیگنال را غیرفعال می‌کند و فقط هزینه‌اش دانه‌بندی timeout resolver است

هر دو نقص پروفایلی مشترک دارند که پیدا کردنشان را بعداً گران می‌کند. هیچ‌کدام در یک تست تابعی مقابل یک سرور خوش‌رفتار روی یک نخ ظاهر نمی‌شوند. هر دو در production ظاهر می‌شوند، مقابل یک TSA خاص، زیر لود. وقتی یک کتابخانه شبکه‌ای را باند می‌کنید، قبل از اینکه فرض کنید پیش‌فرض‌هایش با پروسه شما جورند بخوانید درباره چه چیزی فرض کرده‌اند

نمودار transport یک timestamp با libcurl در PDFium VCL که نشان می‌دهد curl_easy_setopt به‌شکل prototypeهای ثابت پاسکالی گیرنده long و pointer اعلان شده که آرگومان‌ها را در رجیسترهای عدد صحیح پاس می‌دهند، هدر خالی Expect که هندشیک 100-continue یک HTTP را سرکوب می‌کند، CURLOPT_NOSIGNAL که مسیر SIGALRM را روی worker threadها حذف می‌کند، و سقف پاس در سطح transport
دو تنظیم تعیین می‌کنند درخواست کامل می‌شود یا نه: هدر خالی Expect از سرورهایی که هرگز به continuation جواب نمی‌دهند می‌گذرد، و NOSIGNAL timeoutهای resolve کردن نام را از مسیر سیگنال دور نگه می‌دارد در حالی که امضا روی یک worker thread اجرا می‌شود

چطور کدی را وریفای کنید که کامپایلر شما هرگز آن را نمی‌بیند؟

با اینکه به هر حال کامپایلر آن را ببیند، از طریق یک کپی کنترل‌شده. ماشین توسعه اینجا نه کراس‌کامپایلر Linux دارد نه macOS، پس شاخه‌های غیرویندوزی یونیت timestamping در یک بیلد عادی هرگز به code generator نمی‌رسند. کدی که هرگز کامپایل نمی‌شود کدی است که بی‌سروصدا می‌پوسد: یک rename در یک تایپ مشترک، یک لیست پارامتر عوض‌شده، یک وابستگی یونیت اضافه‌شده، و ماه‌ها هیچ‌کس نمی‌فهمد

تکنیک مکانیکی است. یونیت را در یک شاخه موقت کپی کنید، renameش کنید، و هر conditional ویندوزی را، هم فرم {$IFDEF MSWINDOWS} و هم فرم {$IF DEFINED(MSWINDOWS)، با سیمبولی که هرگز تعریف نمی‌شود جایگزین کنید. بعد کپی را کامپایل کنید. وقتی هر 3,828 خط کامپایل شد اثبات کرده‌اید که مسیر غیرویندوزی از یونیت‌های موجود استفاده می‌کند، توابع بک‌اند را با امضاهای منطبق صدا می‌زند، و به تایپ‌هایی ارجاع می‌دهد که در scope هستند. آن اثبات کارکردن transport نیست، و چیزی کوتاه‌تر از خود پلتفرم هدف آن را به شما نمی‌دهد. اثبات این است که شاخه از قبل شکسته نیست، که همان حالت شکستی است که واقعاً انباشته می‌شود

عادت همراهش این است که خود یونیت libcurl را بدون گارد پلتفرمی نگه دارید، تا در بیلد عادی ویندوز هم شرکت کند حتی اگر هیچ‌جای آن به آن ارجاع ندهد. بیلد روزانه بعد سینتکس و تایپ‌هایش را مجانی گارد می‌کند. یونیتی که فقط روی پلتفرمی که ندارید کامپایل می‌شود یونیتی است که هیچ کامپایلری اصلاً چکش نمی‌کند، و همین استدلال در سراسر کار کراس‌کامپایلری که در تله‌های کراس‌کامپایلر Delphi و FPC توصیف شده صدق می‌کند

محدود کردن چیزی که برمی‌گردد

یک پاسخ timestamp یک ساختار DER کوچک است، و هیچ چیز در transport آن را الزامی نمی‌کند. سروری که نفوذ شده، بدتنظیم است، یا صرفاً به URL غلطی اشاره کرده می‌تواند یک stream دلخواه برگرداند، و کلاینتی که تا بسته‌شدن اتصال می‌خواند با کمال میل آن را جمع می‌کند. پس هر دو transport به پاسخ سقف می‌دهند، که جای درست برای این محدودیت همان‌جاست: رد کردن در سطح transport مانع می‌شود بدنه غول‌آسا اصلاً allocate شود، در حالی که یک چک در سطح parser فقط بعد از آنکه حافظه تخصیص یافته فعال می‌شود

همین استدلال درباره URL هم صدق می‌کند. بک‌اند فقط schemeهایی را قبول می‌کند که می‌تواند معنادار درباره‌شان حرف بزند، پس یک اشتباه پیکربندی فوراً با پیام روشنی شکست می‌خورد به‌جای اینکه به libcurl سپرده شود تا هرطور که پشتیبانی پروتکلش اجازه می‌دهد تفسیرش کند

جای transport در قصه امضا کجاست

timestamping قدم اول قصه اعتبارسنجی بلندمدت است نه کل آن. توکن باید به امضا الصاق شود، مواد اعتبارسنجی باید در document security store ثبت شود، و timestampهای آرشیو باید قبل از آنکه تاریخی که الان هست ضعیف شود تمدید شوند. کل این قوس در امضاهای PDF بلندمدت با timestampهای RFC 3161 و DSS پوشش داده شده

نمودار PDFium VCL از یک درخواست timestamp یک RFC 3161 که از DocumentDigest از طریق BuildTimeStampQuery و PostTimeStampQuery روی libcurl به یک سرور TSA می‌رود، پاسخ DER در سطح transport سقف می‌خورد، بعد AttachTimeStampToken تغذیه‌کننده DSS و تمدید timestamp آرشیو در اعتبارسنجی بلندمدت است
timestamping قدم اول قصه اعتبارسنجی بلندمدت است: توکن باید الصاق شود، مواد اعتبارسنجی در document security store ثبت شود، و timestampهای آرشیو قبل از ضعیف‌شدن تاریخی که الان هست تمدید شوند

این transport هم یک تکه از یک موضع portability وسیع‌تر است: native library loaderای که در لود کردن کتابخانه native روی هر هدف توصیف شده همین رده مسئله را برای خود باینری PDFium حل می‌کند. در هر دو حالت الگو یکسان است، باند کردن تعداد کمی سیمبول به‌شکل داینامیک، گزارش دقیق اینکه چه چیزی باند نشد، و هرگز اجازه ندهید یک وابسته غایب به یک شکست لینک-زمانی تبدیل شود که شروع اپلیکیشن را می‌خواباند

بک‌اندهای timestamp ویندوزی و غیرویندوزی هر دو با PDFium Delphi component عرضه می‌شوند، با انتخاب بر اساس هدف نه پیکربندی، پس یک اپلیکیشن Lazarus روی Linux و یک اپلیکیشن دلفی روی Windows امضای timestamped یکسانی از طریق لوله‌کشی متفاوت تولید می‌کنند