Rewrite Mapping

عکس تزئینی است!
عکس تزئینی است!

رفع ادعا: این نوشته را برای اولین بار در وبلاگ خودم به زبان انگلیسی منتشر کردم.

یکی از مسائلی که از مدت‌ها پیش علاقه داشتم که حلش کنم، مشکل ارائه‌ی فایل‌های بزرگ به کلاینت‌های محرز‌شده بود. بیش‌تر راه‌حل‌های این مسئله، از کوکی‌ها یا اسکریپت‌های CGI (مثل PHP) استفاده می‌کردند، اما تصمیم گرفتم تا راه‌حل دیگری برای این مسئله پیدا کنم.

راه‌حلی که یافتم و تصمیم گرفتم این‌جا آن را شرح دهم، تکنیکی به نام «Rewrite Mapping» که وب‌سرور apache2 آن را ارائه می‌دهد. از این تکنیک برای Rewriteکردن مسیر‌ها به صورت پویا استفاده می‌شود که Schemeهای تابع‌وار را در اختیار RewriteRule می‌گذارد.

یک مسئله‌ی ساده‌تر

شاید طرح یک مسئله‌ی ساده‌تر برای فهم بهتر این تکنیک بهتر باشد. تصور کنید که ما یک فروشگاه اینترنتی داریم که محصولات متعددی را می‌فروشیم. برای کمک به SEO فروشگاه، به‌جای استفاده شماره‌ی محصولات، از Slug یا نامک آن‌ها می‌خواهیم استفاده کنیم. رابطه‌ی بین شماره‌ی محصولات و نامک آن‌ها را در قالب یک فایل txt به این‌صورت در اختیار داریم:

television 993
stereo 198
fishingrod 043
basketball 418
telephone 328

حالا، می‌خواهیم هر کاربری که به آدرس product/stereo مراجعه کرد، صفحه‌ی product.php?id=198 برای او نمایش داده شود، همین‌طور درمورد آدرس‌های product/telephone و غیره.

ابتدا یک Scope در محدوده‌ی فراتر از Directory انتخاب می‌کنیم، یعنی فایل‌های .htaccess و کانفیگ‌هایی که در تگ <Directory> انجام می‌شوند موردقبول نیستند، من در تگ VirtualHost این کار را انجام دادم.

برای این که بتوانیم به صورت پویا از فایل txt که بالاتر اشاره کردم استفاده کنیم، نحوه‌ی Map‌کردن را برای وب‌سرور تعریف می‌کنیم:

RewriteMap product2id "txt:/etc/apache2/productmap.txt"

این خط به وب‌سرور دستور می‌دهد که فایل productmap.txt (همان فایل txt بالاتر) را بخواند و اطلاعات آن را به صورت یک Scheme تابع‌وار به نام product2id درآورد. این Scheme، مقدار اول را به مقدار دوم هر سطر مربوط می‌کند. حالا از product2id در RewriteRule استفاده می‌کنیم:

RewriteRule "^/product/(.*)" "/product.php?id=${product2id:$1|NOTFOUND}" [PT]

این خط، در صورت دریافت درخواستی مانند product/stereo از طریق product2id عدد مربوطه رابه دست می‌آورد و به صفحه‌ی صحیح Rewrite می‌کند. اگر شماره‌ی محصول یافت نشد، مقدار NOTFOUND را به جای شماره به فایل product.php می‌فرستد.

مسئله‌ی اصلی

به مسئله‌ی اصلی که در ابتدا اشاره کردم برمی‌گردیم. تعامل با پایگاه‌های داده MySQL و ... به طور مستقیم از apache2 می‌تواند مسئله‌ی وقت‌گیر باشد که در نهایت نتیجه‌ی مطلوب هم به‌دست نیاید. راه‌حل بهتر، متصل‌کردن یک برنامه‌ی خارجی (مثلاً یک اسکریپت پایتون) به apache2 است تا بتوانیم مسیرها را به طور پویاتر، Rewrite کنیم.

خوش‌بختانه، apache2 از پرتکلی برای Mapping به برنامه‌های خارجی پشتیبانی می‌کند. این پرتکل prg نام دارد و مستندات آن به طور کامل موجود است.

فرض کنید که یک فروشگاه محصولات دیجیتال داریم که در ازای دریافت پول، به کاربران اجازه‌ی دانلود فایل‌های بزرگ (مثلاً در حدود ۲ گیگابایت) می‌دهد. ما می‌خواهیم اطمینان حاصل کنیم که خریداران، فایل‌ها را صحیح و سالم دریافت می‌کنند و هم‌چنین امکان توقف/ادامه آن‌ها وجود دارد، بدون این‌که آسیبی به فایل برسد.

معمولاً استفاده از PHP در این‌مورد، منجر به کاهش سرعت دریافت، کاهش احتمال دریافت فایل سالم و هم‌چنین عدم امکان توقف/ادامه آن می‌شود. بنابراین به راه‌حل Rewrite Mapping برمی‌گردیم. به‌جای استفاده از کوکی‌ها و CGI، از Token برای احراز‌هویت کاربر استفاده می‌کنیم.

فرض می‌کنیم که هر مشتری پس از خریداری فایل، یک URL به صورت https://content.our-site.sh/files/bigFile.zip?token={TOKEN} دریافت می‌کند. می‌خواهیم از طریق Rewrite mapping مطمئن شویم که این {TOKEN} صحیح است و اگر صحیح بود، فایل را به کاربر تحویل دهیم و اگر نه، یک فایل دیگر (مثلاً یک فایل HTML) به آن تحویل دهد.

ابتدا Ruleهای مربوط به Mapping را در یک Scope صحیح تعریف می‌کنیم:

RewriteEngine on
RewriteCond %{REQUEST_URI} ^/files # make sure the rule only works for files directory
RewriteMap controller "prg:/home/user/public_html/files/controller.py" www-data:www-data
RewriteRule ^(.*)\.zip$ "${controller:%{REQUEST_URI}?%{QUERY_STRING}}" # rewrite all zip files

خط دوم تعیین می‌کند که این دستورات فقط برای فایل‌های دایرکتوری files صادق هستند، خط سوم به مسیر فایل controller.py که همان برنامه خارجی است و هم‌چنین کاربر و گروه مالک آن اشاره می‌کند. خط سوم، یک Rule برای فایل‌های zip داخل دایرکتوری تعریف می‌کند که یک مقدار مشخص و بر اساس متغیرهای درخواست را به برنامه بفرستد. در این‌جا از دو متغیر خاص REQUEST_URI و QUERY_STRING استفاده کردیم که هر دو مختص به درخواست هستند. هم‌چنین مقدار آن‌ها را با یک علامت‌سؤال جدا کردیم تا توسط کتاب‌خانه‌ی urlparse قابل‌شناسایی باشد، بنابراین ورودی‌ای که برنامه می‌گیرید چیزی شبیه به این خواهد بود:

/files/bigFile.zip?token={TOKEN}

در نهایت، برنامه باید بتواند ورودی را به درستی دریافت و خروجی را چاپ کند. در این مورد، ابتدا مطمئن می‌شویم که TOKEN صحیح است. اگر بود، Query String را از ورودی حذف و باقی‌مانده را چاپ می‌کنیم، در غیر‌این‌صورت به فایل 403 در همان دایرکتوری اشاره می‌کنیم. اسکریپت پایتون ما به این شکل خواهد بود:

#!/usr/bin/env python 
import sys
from urlparse import urlparse

while sys.stdin:
	try:
		strLine = sys.stdin.readline().strip()   ## It is very important to use strip!
		uriComponents = urlparse( strLine )
		if uriComponents.query == "token=correct":
			print uriComponents.path
		else:
			print "/files/403.html"
		sys.stdout.flush()
	except:
		print "NULL"
		sys.stdout.flush()

فراموش نکنید که SHEBANG بسیار مهم است! هم‌چنین اسکریپت باید اجازه‌ی execute را داشته باشد و مالک آن، www-data:www-data باشد.

اگر همه‌چیز را درست انجام داده باشید، فایل‌های zip در دایرکتوری files تنها درصورتی به کاربر تحویل داده می‌شوند که token=correct در Query String درخواست باشد، در غیراین‌صورت فایل 403.html به کاربر فرستاده می‌شود. شما می‌توانید این اسکریپت را به یک پایگاه‌داده یا API خارجی متصل کنید. این دیگر بستگی به خلاقیت شما و محدودیت‌های پایتون دارد.

و در آخر

ارسال فایل‌های بزرگ به کلاینت‌ها همیشه می‌تواند چالش‌بر‌انگیز باشد. در این نوشته با هم این مسئله را با Rewrite Mapping حل کردیم که علاوه بر سرعت بالاتر و انعطاف‌پذیری بیش‌تر، اجازه‌ی استفاده از بقیه‌ی قابلیت‌ها و گزینه‌های apache2 را می‌دهد.