پروژه: ردپای یک درخواست
پروژهی پایانی دورهی مبانی: یک سایت HTTPS کوچک روی کامپیوتر خودت با اسم دامنهی دلخواه، CA آزمایشی و فایل قابلدانلود، دنبال کردن یک درخواست مرحله به مرحله (اسم ← IP ← پورت ← TLS ← HTTP ← لاگ سرور)، اندازهگیری زمان هر مرحله با curl، بررسی سلامت دانلود با هش، و یک اسکریپت تشخیص که میگوید درخواست در کدام مرحله شکست خورده.
- حدود ۲۰ دقیقه مطالعه
- ۳ تمرین با جواب
- مقدماتی (از صفر مطلق)
- بهروزرسانی: ۲۹ مهر ۱۴۰۵
توی این پروژه همهی مفهومهای دوره را کنار هم میبینی. یک «فروشگاه» کوچک روی کامپیوتر خودت راه میاندازی که اسم دامنهی خودش را دارد (shop.test)، با HTTPS و گواهیای که یک CA آزمایشی امضا کرده، و یک فایل برای دانلود با هشش. بعد یک درخواست را از لحظهی تایپ آدرس تا رسیدن پاسخ مرحله به مرحله دنبال میکنی و در آخر اسکریپتی مینویسی که برای هر آدرسی بگوید درخواست در کدام مرحله شکست خورده: اسم، پورت، گواهی یا خود سایت. این همان کاری است که یک مهندس شبکه یا DevOps هر روز با «سایت باز نمیشود» انجام میدهد.
«سایت باز نمیشود» یک جمله است، ولی پشتش دستکم پنج مرحله است که هر کدام میتواند جداگانه خراب شود و هر کدام علامت خودش را دارد. کسی که مرحلهها را نمیشناسد، همهچیز را با هم امتحان میکند: مرورگر را عوض میکند، کش را پاک میکند، سرور را ریاستارت میکند. کسی که میشناسد، از اول مسیر شروع میکند و در یک دقیقه به مرحلهی خراب میرسد.
نقشهی مسیر
Section titled “نقشهی مسیر”پوشهی پروژه:
قدم ۱: اسم دامنه
Section titled “قدم ۱: اسم دامنه”shop.test را در دفترچهی محلی (درس DNS) به کامپیوتر خودت اشاره بده:
echo "127.0.0.1 shop.test" | sudo tee -a /etc/hostsgetent hosts shop.test127.0.0.1 shop.test127.0.0.1 shop.testقدم ۲: CA و گواهی
Section titled “قدم ۲: CA و گواهی”همان کارهای درس HTTPS، این بار برای shop.test:
mkdir -p ~/trace/site && cd ~/traceopenssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \ -keyout ca.key -out ca.crt -days 365 -subj "/CN=LoopX Test CA"openssl req -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \ -keyout shop.key -out shop.csr -subj "/CN=shop.test"printf "subjectAltName=DNS:shop.test\n" > san.extopenssl x509 -req -in shop.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ -out shop.crt -days 90 -extfile san.extCertificate request self-signature oksubject=CN = shop.testقدم ۳: محتوای سایت و فایل دانلودی
Section titled “قدم ۳: محتوای سایت و فایل دانلودی”cat > site/index.html <<EOF<!doctype html><html lang="fa" dir="rtl"><head><meta charset="utf-8"><title>فروشگاه</title></head><body><h1>فروشگاه آزمایشی</h1><a href="catalog.pdf">دانلود کاتالوگ</a></body></html>EOFhead -c 300000 /dev/urandom > site/catalog.pdf(cd site && sha256sum catalog.pdf > SHA256SUMS)ls -l sitetotal 304-rw-rw-r-- 1 ali ali 78 Oct 22 00:40 SHA256SUMS-rw-rw-r-- 1 ali ali 300000 Oct 22 00:40 catalog.pdf-rw-rw-r-- 1 ali ali 219 Oct 22 00:40 index.html/dev/urandom بایتهای تصادفی میدهد؛ «کاتالوگ» ما فقط ۳۰۰ هزار بایت تصادفی است با پسوند .pdf (درس فایل: پسوند فقط اسم است). SHA256SUMS همان فایلی است که سایتهای واقعی کنار فایلهای دانلودی میگذارند (درس رمز و هش). پرانتز دور cd site && ... یعنی «این را در یک پوستهی جدا اجرا کن»، تا خودمان در ~/trace بمانیم.
قدم ۴: سرور
Section titled “قدم ۴: سرور”import functools, http.server, ssl
Handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory="site")server = http.server.ThreadingHTTPServer(("127.0.0.1", 8443), Handler)ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)ctx.load_cert_chain("shop.crt", "shop.key")server.socket = ctx.wrap_socket(server.socket, server_side=True)print("serving https://shop.test:8443 ...", flush=True)server.serve_forever()همان سرور درس HTTPS با دو فرق: پوشهی site را سرو میکند (نه پوشهی فعلی، تا کلید خصوصیها هرگز قابلدانلود نباشند!) و ThreadingHTTPServer چند درخواست را همزمان جواب میدهد. در یک ترمینال جدا روشنش کن و بگذار بماند:
cd ~/trace && python3 server.pyserving https://shop.test:8443 ...قدم ۵: ردپا، مرحله به مرحله
Section titled “قدم ۵: ردپا، مرحله به مرحله”در ترمینال دوم (cd ~/trace)، همان مسیری را برو که مرورگر میرود.
مرحلهی ۱، اسم ← IP (درس DNS):
getent hosts shop.test127.0.0.1 shop.testمرحلهی ۲، اتصال TCP (درس پورت):
nc -zv shop.test 8443Connection to shop.test (127.0.0.1) 8443 port [tcp/*] succeeded!مرحلهی ۳ و ۴، TLS و HTTP (درسهای HTTPS و HTTP). از خروجی بلند curl -v فقط خطهای مهم را نگه دار (grep -E خطهایی را که با این الگوها جورند نشان میدهد؛ دورهی لینوکس):
curl -sv --cacert ca.crt https://shop.test:8443/ 2>&1 \ | grep -E "^\* (Connected|SSL connection)|^\* (subject|issuer|SSL cert)|^[<>] (GET|HTTP|Host|Content-Length|Content-type)"* Connected to shop.test (127.0.0.1) port 8443* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519 / id-ecPublicKey* subject: CN=shop.test* subjectAltName: host "shop.test" matched cert's "shop.test"* issuer: CN=LoopX Test CA* SSL certificate verify ok.> GET / HTTP/1.1> Host: shop.test:8443< HTTP/1.0 200 OK< Content-type: text/html< Content-Length: 219خط به خط، کل دوره در یازده خط: اتصال TCP به IPی که از اسم به دست آمد؛ تونل TLS 1.3 با AES؛ گواهی برای shop.test، امضاشده با CA ما و verify ok؛ و داخل تونل، درخواست HTTP با هدر Host و پاسخ ۲۰۰ با ۲۱۹ بایت، دقیقاً اندازهی index.html.
مرحلهی ۵، لاگ سرور (درس کلاینت و سرور). به ترمینال اول نگاه کن:
127.0.0.1 - - [22/Oct/2026 00:40:36] "GET / HTTP/1.1" 200 -قدم ۶: زمان هر مرحله
Section titled “قدم ۶: زمان هر مرحله”curl میتواند زمان رسیدن به پایان هر مرحله را بگوید (به ثانیه، از لحظهی شروع):
curl -s -o /dev/null --cacert ca.crt \ -w "dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} first-byte=%{time_starttransfer} total=%{time_total}\n" \ https://shop.test:8443/dns=0.000247 tcp=0.000332 tls=0.003766 first-byte=0.008605 total=0.008657همه زیر ۱۰ میلیثانیه، چون همهچیز روی همین کامپیوتر است: DNS از /etc/hosts (چند صدم میلیثانیه)، TCP روی localhost، TLS کمی بیشتر (رمزنگاری کلید عمومی کار CPU است)، و بیشترین زمان تا اولین بایت پاسخ: کار خود سرور پایتون. روی یک سایت واقعی در یک شهر دیگر، هر مرحلهی شبکهای دهها میلیثانیه میشود (درس شبکه، ping). این عددها به تو میگویند کندی کجاست: DNS کند؟ سرور دور؟ یا برنامهی سرور کند (first-byte بزرگ و بقیه کوچک)؟
قدم ۷: دانلود سالم؟
Section titled “قدم ۷: دانلود سالم؟”mkdir -p ~/downloads && cd ~/downloadscurl -sS --cacert ~/trace/ca.crt -O https://shop.test:8443/catalog.pdf -O https://shop.test:8443/SHA256SUMSsha256sum -c SHA256SUMScatalog.pdf: OK(-O فایل را با همان اسمی که در آدرس است ذخیره میکند.) حالا یک «خرابی» شبیهسازی کن، یک بایت به آخر فایل اضافه کن:
printf "x" >> catalog.pdfsha256sum -c SHA256SUMS; echo "exit=$?"file catalog.pdfcatalog.pdf: FAILEDsha256sum: WARNING: 1 computed checksum did NOT matchexit=1catalog.pdf: dataیک بایت از ۳۰۰ هزار، و هش رد شد. file هم میگوید محتوا PDF نیست، فقط «data» است؛ پسوند دروغ میگفت. هر دو درس را اینجا با هم دیدی.
قدم ۸: اسکریپت تشخیص
Section titled “قدم ۸: اسکریپت تشخیص”همهی این مرحلهها را در یک اسکریپت جمع کن که برای هر آدرسی، مرحله به مرحله جلو برود و اولین مرحلهی شکستخورده را بگوید. فایل ~/trace/trace.sh:
#!/usr/bin/env bash# trace.sh: ردپای یک درخواست HTTPS، مرحله به مرحله# استفاده: ./trace.sh https://shop.test:8443/url="$1"host=$(echo "$url" | sed -E 's#^https?://([^/:]+).*#\1#')port=$(echo "$url" | sed -nE 's#^https?://[^/:]+:([0-9]+).*#\1#p')port=${port:-443}
echo "== 1) DNS: $host"ip=$(getent hosts "$host" | awk '{print $1}' | head -1)if [ -z "$ip" ]; then echo " FAIL: name not found (check DNS or /etc/hosts)"; exit 1fiecho " ok: $ip"
echo "== 2) TCP: $ip:$port"if ! nc -z -w 3 "$ip" "$port" 2>/dev/null; then echo " FAIL: port closed or unreachable (is the server running? firewall?)"; exit 2fiecho " ok: port open"
echo "== 3) TLS + 4) HTTP"out=$(curl -sS -o /dev/null --cacert ca.crt \ -w "%{http_code} %{time_namelookup} %{time_connect} %{time_appconnect} %{time_starttransfer} %{time_total} %{size_download}" \ "$url" 2>&1)code=$?if [ $code -ne 0 ]; then echo " FAIL (curl exit $code): $(echo "$out" | head -1)"; exit 3firead -r http dns tcp tls first total size <<< "$out"echo " TLS ok, HTTP status: $http, $size bytes"echo "== timing (seconds from start)"echo " dns=$dns tcp=$tcp tls=$tls first-byte=$first total=$total"[ "$http" -lt 400 ] || { echo " FAIL: server answered with an error"; exit 4; }echo "== all good"لازم نیست همهی خطها را الان بفهمی؛ دورههای لینوکس و bash دقیقاً همین ابزارها را یاد میدهند (sed برای بیرون کشیدن اسم و پورت از آدرس، awk برای ستون اول، if و کد خروج). چیزی که مهم است منطق است: هر مرحله فقط وقتی اجرا میشود که قبلی موفق بوده، و هر شکست کد خروج متفاوتی دارد (۱ تا ۴) تا اسکریپتهای دیگر هم بفهمند چه شده.
cd ~/tracechmod +x trace.sh./trace.sh https://shop.test:8443/; echo "exit=$?"== 1) DNS: shop.test ok: 127.0.0.1== 2) TCP: 127.0.0.1:8443 ok: port open== 3) TLS + 4) HTTP TLS ok, HTTP status: 200, 219 bytes== timing (seconds from start) dns=0.000247 tcp=0.000332 tls=0.003766 first-byte=0.008605 total=0.008657== all goodexit=0حالا چهار خرابی متفاوت، همان سه پیام سناریو و یکی بیشتر:
./trace.sh https://shopp.test:8443/; echo "exit=$?"./trace.sh https://shop.test:9999/; echo "exit=$?"./trace.sh https://localhost:8443/; echo "exit=$?"./trace.sh https://shop.test:8443/missing.html; echo "exit=$?"== 1) DNS: shopp.test FAIL: name not found (check DNS or /etc/hosts)exit=1== 1) DNS: shop.test ok: 127.0.0.1== 2) TCP: 127.0.0.1:9999 FAIL: port closed or unreachable (is the server running? firewall?)exit=2== 1) DNS: localhost ok: 127.0.0.1== 2) TCP: 127.0.0.1:8443 ok: port open== 3) TLS + 4) HTTP FAIL (curl exit 60): curl: (60) SSL: no alternative certificate subject name matches target host name 'localhost'exit=3== 1) DNS: shop.test ok: 127.0.0.1== 2) TCP: 127.0.0.1:8443 ok: port open== 3) TLS + 4) HTTP TLS ok, HTTP status: 404, 335 bytes== timing (seconds from start) dns=0.000424 tcp=0.000511 tls=0.003495 first-byte=0.004509 total=0.004545 FAIL: server answered with an errorexit=4| آدرس | مرحلهی شکست | علت | درس |
|---|---|---|---|
shopp.test |
۱ (اسم) | غلط تایپی؛ این اسم در hosts/DNS نیست | DNS |
shop.test:9999 |
۲ (TCP) | روی این پورت کسی گوش نمیدهد | پورت |
localhost:8443 |
۳ (TLS) | همان سرور، ولی گواهی برای shop.test است |
HTTPS |
shop.test:8443/missing.html |
۴ (HTTP) | همهچیز سالم، فقط این صفحه نیست | HTTP |
سومی جالب است: اسم درست بود، پورت باز بود، همان سرور بود؛ فقط اسمی که پرسیدی با گواهی نمیخواند. بدون این اسکریپت، «localhost کار نمیکند ولی shop.test کار میکند» گیجکننده بود.
trace.sh برای سایت شرکت در مرحلهی ۲ با «port closed» شکست میخورد. اولین کاری که روی سرور میکنی؟
جواب درست: ببین برنامهی وبسرور در حال اجراست و روی کدام آدرس و پورت گوش میدهد (ss -tln)
مرحلهی ۱ سالم بود؛ مشکل از سرور یا فایروال است، نه اسم یا گواهی.
چکلیست نهایی
Section titled “چکلیست نهایی”اشتباهات رایج
Section titled “اشتباهات رایج”۱) سرو کردن پوشهای که کلید خصوصی دارد
Section titled “۱) سرو کردن پوشهای که کلید خصوصی دارد”اگر server.py پوشهی ~/trace را سرو میکرد، هر کس https://shop.test:8443/shop.key را دانلود میکرد. راهحل: محتوای سایت همیشه در پوشهی جدا؛ کلیدها با مجوز 600 بیرون از آن.
۲) پریدن به مرحلهی آخر
Section titled “۲) پریدن به مرحلهی آخر”«۵۰۰ میدهد، پس DNS را عوض کنیم». اگر پاسخ HTTP گرفتی، مرحلههای ۱ تا ۳ سالماند. راهحل: اولین مرحلهی خراب را پیدا کن و فقط همان را درست کن.
۳) -k برای «حل» مرحلهی ۳
Section titled “۳) -k برای «حل» مرحلهی ۳”اسکریپت با curl -k همیشه از مرحلهی ۳ رد میشود و تو هیچوقت نمیفهمی گواهی منقضی یا اشتباه است. راهحل: در اسکریپتهای بررسی، بررسی گواهی را روشن نگه دار.
۴) فراموش کردن پاکسازی
Section titled “۴) فراموش کردن پاکسازی”خط shop.test ماهها در /etc/hosts میماند. راهحل: آخر پروژه پاکش کن (sudo nano /etc/hosts).
سرور را با Ctrl+C ببند و trace.sh را برای https://shop.test:8443/ اجرا کن. در کدام مرحله شکست میخورد؟ خطای مرورگر برای همین وضعیت چیست؟
دیدن جواب
== 1) DNS: shop.test ok: 127.0.0.1== 2) TCP: 127.0.0.1:8443 FAIL: port closed or unreachable (is the server running? firewall?)exit=2مرحلهی ۲: اسم سالم است ولی روی پورت ۸۴۴۳ برنامهای نیست. مرورگر میگوید ERR_CONNECTION_REFUSED («shop.test refused to connect»)؛ curl میگوید Couldn't connect to server. همان درس پورت.
یک خرابی مرحلهی ۳ دیگر بساز: گواهیای برای shop.test که CA ما امضا نکرده (خودامضا). سرور را با آن روشن کن و ببین trace.sh چه میگوید. فرقش با خطای localhost:8443 چیست؟
دیدن جواب
cd ~/traceopenssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \ -keyout self.key -out self.crt -days 30 -subj "/CN=shop.test" -addext "subjectAltName=DNS:shop.test"sed -e "s/shop.crt/self.crt/; s/shop.key/self.key/" server.py > server_self.pypython3 server_self.pyدر ترمینال دیگر:
./trace.sh https://shop.test:8443/; echo "exit=$?"== 3) TLS + 4) HTTP FAIL (curl exit 60): curl: (60) SSL certificate problem: self-signed certificateexit=3هر دو در مرحلهی ۳ و با کد ۶۰ curl، ولی علت متفاوت: اینجا اسم درست است و گواهی به هیچ CA مورد اعتمادی نمیرسد؛ در localhost گواهی معتبر بود ولی اسم نمیخواند. در دنیای واقعی، اولی یعنی «کسی گواهی آزمایشی را روی سرور واقعی گذاشته» (یا یک مهاجم وسط راه است) و دومی یعنی «اسم سایت در گواهی نیست».
trace.sh را گسترش بده تا یک مرحلهی ۰ هم داشته باشد: اگر آدرس با http:// (نه https) شروع شد، هشدار بدهد «بدون رمزنگاری» و پورت پیشفرض را ۸۰ بگذارد. و در آخر، اگر گواهی کمتر از ۳۰ روز اعتبار دارد، هشدار بدهد (راهنما: openssl s_client گواهی را از سرور میگیرد و openssl x509 -checkend آن را بررسی میکند؛ درس HTTPS).
دیدن جواب
قبل از مرحلهی ۱:
scheme=${url%%://*}if [ "$scheme" = "http" ]; then echo "== 0) WARNING: plain http, nothing is encrypted" port=$(echo "$url" | sed -nE 's#^https?://[^/:]+:([0-9]+).*#\1#p') port=${port:-80}fiو قبل از == all good:
if [ "$scheme" = "https" ]; then if ! openssl s_client -connect "$ip:$port" -servername "$host" </dev/null 2>/dev/null \ | openssl x509 -noout -checkend $((30*86400)) > /dev/null; then echo " WARNING: certificate expires in less than 30 days" fifi./trace.sh https://shop.test:8443/ dns=0.000313 tcp=0.000476 tls=0.003333 first-byte=0.008898 total=0.008935== all goodگواهی shop.crt نود روز اعتبار دارد، پس هشداری نیامد. برای دیدن هشدار، یک گواهی ۲۰ روزه با همان CA بساز و سرور را با آن اجرا کن:
openssl x509 -req -in shop.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out short.crt -days 20 -extfile san.extsed -e "s/shop.crt/short.crt/" server.py > server_short.pypython3 server_short.py dns=0.000317 tcp=0.000413 tls=0.003900 first-byte=0.009083 total=0.009120 WARNING: certificate expires in less than 30 days== all good(گواهی خودامضای تمرین متوسط به این مرحله نمیرسد: در مرحلهی ۳ رد میشود.) برای آدرس http:// هم هشدار مرحلهی ۰ میآید و tls=0.000000 است، چون دستدادن TLSی در کار نیست؛ پیام «TLS ok» را هم میتوانی فقط برای https چاپ کنی. ${url%%://*} (برش رشته در bash) همهچیز بعد از :// را حذف میکند و http یا https میماند. -servername همان SNI درس HTTPS است. حالا یک ابزار واقعی داری: همین اسکریپت را با cron هر ساعت برای سایتهایت اجرا کن و اگر کد خروجش صفر نبود، خبرت کند (دورهی لینوکس، درس cron).
آزمونک
Section titled “آزمونک”ترتیب درست مرحلههای یک درخواست HTTPS؟
جواب درست: DNS، TCP، TLS، HTTP
اول اسم، بعد اتصال، بعد تونل امن، بعد درخواست.
اگر پاسخ HTTP با کد 500 گرفتی، کدام مرحلهها قطعاً سالم بودهاند؟
جواب درست: DNS، TCP و TLS
بدون آنها پاسخ HTTP نمیرسید.
خطای «could not resolve host» در کدام مرحله است؟
جواب درست: اسم ← IP
درس DNS.
time_starttransfer خیلی بزرگ و بقیهی زمانها کوچکاند. کندی کجاست؟
جواب درست: خود برنامهی سرور در ساختن پاسخ
تا اولین بایت پاسخ، کار سرور است.
چرا server.py پوشهی site را سرو میکند نه ~/trace را؟
جواب درست: تا کلیدهای خصوصی قابلدانلود نباشند
محتوای عمومی جدا از اسرار.
sha256sum -c برای فایل دانلودشده FAILED داد. یعنی…
جواب درست: فایل با چیزی که سایت هشش را منتشر کرده فرق دارد: خراب یا دستکاریشده
یک بایت فرق هم کافی است.
جمعبندی
Section titled “جمعبندی”- یک درخواست HTTPS پنج مرحله دارد: اسم ← IP، TCP، TLS، HTTP، و ردپا در لاگ سرور؛ هر کدام خطای مخصوص خودش را دارد.
- تشخیص یعنی پیدا کردن اولین مرحلهی خراب:
getent hosts،nc -zv،curl -v، کد وضعیت، لاگ. curl -wزمان هر مرحله را میدهد و میگوید کندی در شبکه است یا در برنامه.- هش (
SHA256SUMS) سلامت فایل دانلودی را تضمین میکند؛ پسوند فایل چیزی را تضمین نمیکند. - اسکریپت
trace.shهمهی دوره را در یک ابزار جمع کرد؛ در دورههای لینوکس و bash یاد میگیری هر خطش را خودت بنویسی. - قدم بعدی: دورهی لینوکس از صفر؛ جایی که این ابزارها را واقعاً در دست میگیری.
| دستور | کاری که میکند |
|---|---|
getent hosts name | مرحلهی ۱: اسم ← IP |
nc -zv host port | مرحلهی ۲: پورت باز است؟ |
curl -v --cacert ca.crt https://... | مرحلهی ۳ و ۴: TLS و HTTP |
curl -w "%{time_connect} %{time_appconnect} %{time_starttransfer}" | زمان هر مرحله |
curl -O URL | دانلود با همان اسم |
sha256sum -c SHA256SUMS | بررسی سلامت دانلود |
curl exit 6 / 7 / 60 / 28 | اسم / اتصال / گواهی / timeout |
4xx / 5xx | مشکل درخواست / مشکل سرور |