رفتن به محتوا
مبانی کامپیوتر و اینترنتسرور، ابر و امنیت پایهدرس ۱۶ از ۱۶

پروژه: ردپای یک درخواست

پروژه‌ی پایانی دوره‌ی مبانی: یک سایت HTTPS کوچک روی کامپیوتر خودت با اسم دامنه‌ی دلخواه، CA آزمایشی و فایل قابل‌دانلود، دنبال کردن یک درخواست مرحله به مرحله (اسم ← IP ← پورت ← TLS ← HTTP ← لاگ سرور)، اندازه‌گیری زمان هر مرحله با curl، بررسی سلامت دانلود با هش، و یک اسکریپت تشخیص که می‌گوید درخواست در کدام مرحله شکست خورده.

  • حدود ۲۰ دقیقه مطالعه
  • ۳ تمرین با جواب
  • مقدماتی (از صفر مطلق)
  • به‌روزرسانی: ۲۹ مهر ۱۴۰۵

توی این پروژه همه‌ی مفهوم‌های دوره را کنار هم می‌بینی. یک «فروشگاه» کوچک روی کامپیوتر خودت راه می‌اندازی که اسم دامنه‌ی خودش را دارد (shop.test)، با HTTPS و گواهی‌ای که یک CA آزمایشی امضا کرده، و یک فایل برای دانلود با هشش. بعد یک درخواست را از لحظه‌ی تایپ آدرس تا رسیدن پاسخ مرحله به مرحله دنبال می‌کنی و در آخر اسکریپتی می‌نویسی که برای هر آدرسی بگوید درخواست در کدام مرحله شکست خورده: اسم، پورت، گواهی یا خود سایت. این همان کاری است که یک مهندس شبکه یا DevOps هر روز با «سایت باز نمی‌شود» انجام می‌دهد.

«سایت باز نمی‌شود» یک جمله است، ولی پشتش دست‌کم پنج مرحله است که هر کدام می‌تواند جداگانه خراب شود و هر کدام علامت خودش را دارد. کسی که مرحله‌ها را نمی‌شناسد، همه‌چیز را با هم امتحان می‌کند: مرورگر را عوض می‌کند، کش را پاک می‌کند، سرور را ری‌استارت می‌کند. کسی که می‌شناسد، از اول مسیر شروع می‌کند و در یک دقیقه به مرحله‌ی خراب می‌رسد.

ردپای یک درخواست HTTPS: هر مرحله به مرحله‌ی قبلی تکیه دارد، و هر کدام خطای مخصوص خودش را دارد.

پوشه‌ی پروژه:

shop.test را در دفترچه‌ی محلی (درس DNS) به کامپیوتر خودت اشاره بده:

Terminal window
echo "127.0.0.1 shop.test" | sudo tee -a /etc/hosts
getent hosts shop.test
خروجی
127.0.0.1 shop.test
127.0.0.1 shop.test

همان کارهای درس HTTPS، این بار برای shop.test:

Terminal window
mkdir -p ~/trace/site && cd ~/trace
openssl 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.ext
openssl x509 -req -in shop.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-out shop.crt -days 90 -extfile san.ext
خروجی (بخش آخر)
Certificate request self-signature ok
subject=CN = shop.test

قدم ۳: محتوای سایت و فایل دانلودی

Section titled “قدم ۳: محتوای سایت و فایل دانلودی”
Terminal window
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>
EOF
head -c 300000 /dev/urandom > site/catalog.pdf
(cd site && sha256sum catalog.pdf > SHA256SUMS)
ls -l site
خروجی
total 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 بمانیم.

server.py
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 چند درخواست را هم‌زمان جواب می‌دهد. در یک ترمینال جدا روشنش کن و بگذار بماند:

Terminal window
cd ~/trace && python3 server.py
خروجی
serving https://shop.test:8443 ...

قدم ۵: ردپا، مرحله به مرحله

Section titled “قدم ۵: ردپا، مرحله به مرحله”

در ترمینال دوم (cd ~/trace)، همان مسیری را برو که مرورگر می‌رود.

مرحله‌ی ۱، اسم ← IP (درس DNS):

Terminal window
getent hosts shop.test
خروجی
127.0.0.1 shop.test

مرحله‌ی ۲، اتصال TCP (درس پورت):

Terminal window
nc -zv shop.test 8443
خروجی
Connection to shop.test (127.0.0.1) 8443 port [tcp/*] succeeded!

مرحله‌ی ۳ و ۴، TLS و HTTP (درس‌های HTTPS و HTTP). از خروجی بلند curl -v فقط خط‌های مهم را نگه دار (grep -E خط‌هایی را که با این الگوها جورند نشان می‌دهد؛ دوره‌ی لینوکس):

Terminal window
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 -

curl می‌تواند زمان رسیدن به پایان هر مرحله را بگوید (به ثانیه، از لحظه‌ی شروع):

Terminal window
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 بزرگ و بقیه کوچک)؟

Terminal window
mkdir -p ~/downloads && cd ~/downloads
curl -sS --cacert ~/trace/ca.crt -O https://shop.test:8443/catalog.pdf -O https://shop.test:8443/SHA256SUMS
sha256sum -c SHA256SUMS
خروجی
catalog.pdf: OK

(-O فایل را با همان اسمی که در آدرس است ذخیره می‌کند.) حالا یک «خرابی» شبیه‌سازی کن، یک بایت به آخر فایل اضافه کن:

Terminal window
printf "x" >> catalog.pdf
sha256sum -c SHA256SUMS; echo "exit=$?"
file catalog.pdf
خروجی
catalog.pdf: FAILED
sha256sum: WARNING: 1 computed checksum did NOT match
exit=1
catalog.pdf: data

یک بایت از ۳۰۰ هزار، و هش رد شد. file هم می‌گوید محتوا PDF نیست، فقط «data» است؛ پسوند دروغ می‌گفت. هر دو درس را این‌جا با هم دیدی.

همه‌ی این مرحله‌ها را در یک اسکریپت جمع کن که برای هر آدرسی، مرحله به مرحله جلو برود و اولین مرحله‌ی شکست‌خورده را بگوید. فایل ~/trace/trace.sh:

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 1
fi
echo " 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 2
fi
echo " 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 3
fi
read -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 و کد خروج). چیزی که مهم است منطق است: هر مرحله فقط وقتی اجرا می‌شود که قبلی موفق بوده، و هر شکست کد خروج متفاوتی دارد (۱ تا ۴) تا اسکریپت‌های دیگر هم بفهمند چه شده.

Terminal window
cd ~/trace
chmod +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 good
exit=0

حالا چهار خرابی متفاوت، همان سه پیام سناریو و یکی بیشتر:

Terminal window
./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 error
exit=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» شکست می‌خورد. اولین کاری که روی سرور می‌کنی؟

✓ چک‌لیست پروژه‌ی ردپای یک درخواست

۱) سرو کردن پوشه‌ای که کلید خصوصی دارد

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 چیست؟

دیدن جواب
Terminal window
cd ~/trace
openssl 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.py
python3 server_self.py

در ترمینال دیگر:

Terminal window
./trace.sh https://shop.test:8443/; echo "exit=$?"
خروجی (بخش آخر)
== 3) TLS + 4) HTTP
FAIL (curl exit 60): curl: (60) SSL certificate problem: self-signed certificate
exit=3

هر دو در مرحله‌ی ۳ و با کد ۶۰ curl، ولی علت متفاوت: این‌جا اسم درست است و گواهی به هیچ CA مورد اعتمادی نمی‌رسد؛ در localhost گواهی معتبر بود ولی اسم نمی‌خواند. در دنیای واقعی، اولی یعنی «کسی گواهی آزمایشی را روی سرور واقعی گذاشته» (یا یک مهاجم وسط راه است) و دومی یعنی «اسم سایت در گواهی نیست».

تمرینسخت

trace.sh را گسترش بده تا یک مرحله‌ی ۰ هم داشته باشد: اگر آدرس با http:// (نه https) شروع شد، هشدار بدهد «بدون رمزنگاری» و پورت پیش‌فرض را ۸۰ بگذارد. و در آخر، اگر گواهی کمتر از ۳۰ روز اعتبار دارد، هشدار بدهد (راهنما: openssl s_client گواهی را از سرور می‌گیرد و openssl x509 -checkend آن را بررسی می‌کند؛ درس HTTPS).

دیدن جواب

قبل از مرحله‌ی ۱:

Terminal window
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:

Terminal window
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"
fi
fi
Terminal window
./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 بساز و سرور را با آن اجرا کن:

Terminal window
openssl x509 -req -in shop.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out short.crt -days 20 -extfile san.ext
sed -e "s/shop.crt/short.crt/" server.py > server_short.py
python3 server_short.py
خروجی (trace.sh، بخش آخر)
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).

آزمونک
  1. ترتیب درست مرحله‌های یک درخواست HTTPS؟

  2. اگر پاسخ HTTP با کد 500 گرفتی، کدام مرحله‌ها قطعاً سالم بوده‌اند؟

  3. خطای «could not resolve host» در کدام مرحله است؟

  4. time_starttransfer خیلی بزرگ و بقیه‌ی زمان‌ها کوچک‌اند. کندی کجاست؟

  5. چرا server.py پوشه‌ی site را سرو می‌کند نه ~/trace را؟

  6. sha256sum -c برای فایل دانلودشده FAILED داد. یعنی…

  • یک درخواست 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مشکل درخواست / مشکل سرور