آزمایش HTTPS مستقیم در Node.js؛ گواهی محلی و تحویل پروژه
در این درس ۶ بخش
اکنون پروژهٔ سادهٔ ما صفحه دارد، API پاسخ میدهد و برای انتشار عمومی دو مسیر TLS را میشناسیم. در این درس یک آزمایش اختیاری انجام میدهیم: همان برنامه را بدون Nginx مستقیماً با ماژول HTTPS داخلی Node روی دستگاه آزمایشی اجرا میکنیم. هدف دیدن رابطهٔ گواهی، کلید، نام مقصد و درخواست است؛ این آزمایش جای مسیر عمومی درسهای پنجم تا هفتم را نمیگیرد. نتیجه یک نشانی localhost روی پورت ۳۴۴۳ است که فقط از همان میزبان قابل دسترسی است. برای شرکت در آزمایش، دامنه، حساب مرجع صدور و بازکردن پورت اینترنتی لازم نیست.
گواهی خودامضا چه چیزی را نشان میدهد و چه چیزی را نه؟
خودامضا یعنی امضای گواهی با کلید مربوط به خودش انجام شده است؛ مرجع عمومی مورد اعتماد مشتری هویت آن را تأیید نکرده است. این گواهی میتواند برای یادگیری رمزگذاری و تطبیق نام مفید باشد، اما بازدیدکنندهٔ عادی نباید از روی آن به هویت سایت عمومی اعتماد کند. در این تمرین فقط به یک فرمان curl مشخص میگوییم همین گواهی آزمایشگاهی را بشناسد. اعتماد سیستم عامل یا مرورگر را تغییر نمیدهیم و هشدار مرورگر را رد نمیکنیم. با پایان فرمان، تنظیم اعتماد همان فرمان نیز تمام میشود؛ فایل گواهی به ریشهٔ عمومی اینترنت تبدیل نشده است.
localhost نام قراردادی دستگاه خودتان است. اگر در Bash سرور اوبونتو curl اجرا کنید، localhost همان سرور است؛ اگر نشانی را در ویندوز لپتاپ باز کنید، به لپتاپ اشاره میکند. بنابراین این آزمایش را با دو ترمینال روی یک دستگاه انجام دهید. آدرس 127.0.0.1 نیز لوپبک است و برای ارتباط داخلی همان میزبان استفاده میشود. یک دامنهٔ واقعی اینترنتی، IP عمومی و نام محلی سه مقدار قابل جایگزینی بیقید نیستند. گواهی آزمایش، هر دو نام localhost و IP لوپبک را در SAN دارد تا تفاوت نام و نشانی را آگاهانه بررسی کنیم.
اگر اجرای نخستین سرور یا تفاوت پاسخ HTTP و خطای شبکه هنوز روشن نیست، این دو درس قبلی مسیر جبراناند:
https://www.darsavand.com/courses/nodejs-windows-backend-start?lesson=8aaffcaf-6186-4ab5-9ffd-7a2ee33271a6
https://www.darsavand.com/courses/nodejs-backend-ubuntu-deployment?lesson=4529b889-fc8c-493e-bf2b-fd47eb5c9231
در ادامه، فرض میکنیم پروژهٔ کامل hello-http روی همین اوبونتوی آزمایشی در پوشهٔ شخصی شما موجود است و Node با مسیر ثابت دوره اجرا میشود. اگر فقط پروژه را در /opt دارید، برای این آزمایش نسخهٔ قابل مطالعه را به پوشهٔ شخصی جدا منتقل کنید؛ فایل کلید آزمایشگاهی را در پوشهٔ کد منتشرشده قرار ندهید.
گام اول؛ فایل کوچک HTTPS را کنار پروژه بسازید
در پوشهٔ اصلی پروژه، پوشهٔ examples و فایل local-https.mjs را با متن کامل زیر بسازید. ماژول #modules-import-export اجازه میدهد منطق پاسخگویی را از app.mjs دوباره استفاده کنیم؛ نسخهٔ تازهای از مسیرها و اعتبارسنجی اختراع نمیکنیم. createHandler بهصورت ناهمگام فایلهای صفحه را میخواند و تابع پاسخگو میدهد. #async-await این انتظار را خوانا میکند و Promise.all خواندن دادههای مستقل را کنار هم آغاز میکند. سپس createServer متعلق به node:https، همان تابع را پشت TLS قرار میدهد. فایلهای کلید و گواهی از آرگومان مسیر خوانده میشوند؛ محتوی آنها در کد نوشته نمیشود.
import { createServer } from 'node:https';
import { readFile } from 'node:fs/promises';
import { createHandler } from '../app.mjs';
const [keyPath, certPath] = process.argv.slice(2);
if (!keyPath || !certPath || process.argv.length !== 4) {
console.error('Usage: node examples/local-https.mjs KEY_FILE CERT_FILE');
process.exitCode = 1;
} else {
try {
const [key, cert, handler] = await Promise.all([
readFile(keyPath), readFile(certPath), createHandler()
]);
const server = createServer({
key, cert, minVersion: 'TLSv1.2',
requestTimeout: 15000, headersTimeout: 10000, maxHeaderSize: 8192
}, handler);
server.on('error', error => {
console.error(`Cannot start HTTPS lab: ${error.code ?? 'UNKNOWN'}`);
process.exitCode = 1;
});
server.listen(3443, '127.0.0.1', () => {
console.log('HTTPS lab: https://localhost:3443');
});
let stopping = false;
const stop = () => {
if (stopping) return;
stopping = true;
server.close();
const deadline = setTimeout(() => server.closeAllConnections(), 3000);
deadline.unref();
};
process.on('SIGINT', stop);
process.on('SIGTERM', stop);
} catch (error) {
console.error(`Cannot load HTTPS lab: ${error.code ?? 'INVALID_TLS_INPUT'}`);
process.exitCode = 1;
}
}این فایل عمداً پورت ۳۴۴۳ را فقط روی 127.0.0.1 میگیرد. لازم نیست برنامه با root اجرا شود و لازم نیست UFW برای این پورت باز شود. نبود آرگومان، فایل گمشده یا جفت TLS نامعتبر باید پیام شکست بدهد؛ موفقیت فقط چاپ عبارت زیبای HTTPS نیست. هنگام Ctrl+C، سرور دریافت اتصال تازه را متوقف میکند و برای بستهشدن اتصالهای موجود فرصت محدودی میدهد. برنامهٔ اصلی HTTP میتواند روی ۳۰۰۰ باقی بماند چون پورت آزمایش فرق دارد؛ اگر ۳۴۴۳ قبلاً اشغال است، صاحب فرایند را پیدا کنید و همهٔ Nodeهای دستگاه را بهطور گروهی متوقف نکنید.
گام دوم؛ یک گواهی محلی در پوشهٔ خصوصی بسازید
این بلوک را در Bash همان اوبونتو اجرا کنید. openssl باید در دسترس باشد؛ با openssl version نسخه را بخوانید و اگر فرمان وجود ندارد، بستهٔ رسمی openssl را با مدیر بستهٔ سیستم نصب کنید. mktemp مسیر تازهای میسازد و umask دسترسی فایلهای جدید را محدود میکند. گواهی دو روزه صرفاً انتخاب آزمایش است و هیچ ادعایی دربارهٔ عمر گواهیهای عمومی ندارد. گزینهٔ noenc کلید را بدون رمز عبور فایل میسازد تا نمونه بتواند آن را بخواند؛ حفاظت این فایل در آزمایش از مجوز پوشه و فایل میآید. آن را به مخزن پروژه اضافه نکنید.
# روی اوبونتو، Bash؛ گواهی فقط برای این آزمایش محلی است.
(
set -eu
umask 077
TLSDIR=$(mktemp -d "$HOME/hello-local-tls.XXXXXX")
openssl req -x509 -newkey rsa:2048 -sha256 -days 2 -noenc \
-keyout "$TLSDIR/lab-key.pem" -out "$TLSDIR/lab-cert.pem" \
-subj '/CN=localhost' \
-addext 'subjectAltName=DNS:localhost,IP:127.0.0.1' \
-addext 'basicConstraints=critical,CA:FALSE' \
-addext 'extendedKeyUsage=serverAuth'
openssl x509 -in "$TLSDIR/lab-cert.pem" -noout -subject -dates -ext subjectAltName
printf 'Local TLS directory: %s\n' "$TLSDIR"
)در خروجی باید نام localhost، آدرس لوپبک و تاریخهای گواهی خودتان را ببینید. مسیر آخر را یادداشت کنید. اگر فرمان ساخت شکست خورد، بهجای ادامه با فایل نیمهکاره، پیام را بررسی کنید و در یک پوشهٔ تازه دوباره بسازید. فایل عمومی lab-cert.pem را برای بررسی اعتماد فرمان بعد به کار میبریم؛ lab-key.pem خصوصی است. در این آزمایش، گواهی CA عمومی ساخته نشده و دستگاه شما صادرکنندهٔ مورد اعتماد کاربران دیگر نشده است. نبود هزینه یا ثبتنام، به معنی مناسببودن همین فایل برای سایت عمومی نیست.
در ترمینال اول، ابتدا به پوشهٔ اصلی پروژه بروید؛ همان جایی که package.json و app.mjs هستند. مقدار TLSDIR را با مسیر واقعی چاپشده عوض کنید. پرانتز استفاده نکردهایم چون میخواهیم برنامه در پیشزمینه باز بماند و خروجیاش را ببینیم. تا وقتی سرور کار میکند، بازنگشتن خط فرمان هنگ نیست. متن HTTPS lab نشانی مورد انتظار را نشان میدهد. اگر Cannot load یا Cannot start دیدید، قبل از بازکردن ترمینال آزمون، فایلها، آرگومانها و اشغالنبودن پورت را بررسی کنید.
# ترمینال اول اوبونتو، در پوشهٔ اصلی hello-http؛ مسیر را واقعی کنید.
TLSDIR='/home/labadmin/hello-local-tls.ABC123'
/opt/node-current/bin/node examples/local-https.mjs "$TLSDIR/lab-key.pem" "$TLSDIR/lab-cert.pem"گام سوم؛ همان صفحه و API را با اعتماد محدود آزمایش کنید
ترمینال دوم را روی همان اوبونتو باز کنید و مسیر TLSDIR را دوباره بنویسید؛ متغیر ترمینال اول خودکار به ترمینال دوم منتقل نمیشود. در فرمان زیر cacert فایل اعتماد همین درخواست را تعیین میکند، نه اینکه بررسی گواهی را خاموش کند. خروجی وضعیت باید #json معتبر با ok برابر true و نام darsavand-hello باشد. فرمان دوم درخواست واقعی POST با نام ساختگی میفرستد و پاسخ باید سلام به آوند باشد. مسیر نسبی API در فایل صفحه نیز با HTTPS همان مبدأ کار میکند؛ لازم نیست نام دامنه یا کلید TLS را داخل public/app.js بنویسید.
# ترمینال دوم همان اوبونتو؛ مسیر را واقعی کنید.
TLSDIR='/home/labadmin/hello-local-tls.ABC123'
curl --fail --show-error --silent --connect-timeout 3 --max-time 8 \
--cacert "$TLSDIR/lab-cert.pem" https://localhost:3443/api/status
curl --fail --show-error --silent --connect-timeout 3 --max-time 8 \
--cacert "$TLSDIR/lab-cert.pem" -H 'Content-Type: application/json' \
--data-binary '{"name":"آوند"}' https://localhost:3443/api/greet
curl --fail --show-error --silent --connect-timeout 3 --max-time 8 \
--cacert "$TLSDIR/lab-cert.pem" https://localhost:3443/فرمان سوم HTML صفحه را چاپ میکند؛ curl مرورگر نیست و ظاهر و اجرای جاوااسکریپت را رندر نمیکند. برای بررسی تعاملی رابط، صفحهٔ اصلی HTTP محلی درس چهارم یا نشانی HTTPS معتبر عمومی درس ششم یا هفتم را در مرورگر باز کنید. اگر curl بدون cacert اجرا شود، انتظار داریم به این گواهی خودامضا اعتماد نکند. خطای اعتماد، شکست آزمایش نیست؛ بخشی از چیزی است که میخواستیم مشاهده کنیم. برای گرفتن خروجی سبز، بررسی TLS را با گزینهٔ ناامن حذف نکنید. درستبودن #fetch-http-json در برنامه، خطای هویت TLS را جبران نمیکند.
تمرین نام مقصد: با resolve، نام wrong.example را فقط برای همین فرمان به لوپبک وصل کنید. گزینهٔ noproxy نیز همین نام آزمایشی را از پراکسی احتمالی محیط کنار میگذارد تا درخواست واقعاً به لوپبک برود؛ مقصد بیرونی در این فرمان نداریم. همان فایل گواهی مورد اعتماد فرمان است، اما نام اشتباه باید رد شود. این نشان میدهد «به این امضا اعتماد دارم» با «این گواهی برای نام مقصد است» دو شرط متفاوتاند. در پایان، در ترمینال اول Ctrl+C بزنید و دوباره وضعیت را درخواست کنید؛ این بار باید خطای اتصال بگیرید، نه خطای JSON. فایلهای آزمایشی در پوشهٔ خصوصی خودتان میمانند؛ اگر خواستید پاکشان کنید، ابتدا همان مسیر یادداشتشده را دقیق بررسی کنید و هیچ مسیر سیستمی را با الگوی کلی حذف نکنید.
# ترمینال دوم، همان TLSDIR واقعی؛ این فرمان باید خطای نام گواهی بدهد.
curl --show-error --silent --connect-timeout 3 --max-time 8 \
--cacert "$TLSDIR/lab-cert.pem" \
--noproxy wrong.example --resolve wrong.example:3443:127.0.0.1 https://wrong.example:3443/api/statusتحویل نهایی؛ ده سناریو با پاسخ راهنما
۱. صفحهٔ اصلی باز و فایلهای CSS و JS پاسخ ۲۰۰ دارند؛ اگر ظاهر خام است، مسیر و نوع پاسخ فایل را بررسی کنید، نه کلید TLS. ۲. نام «آوند» پیام واقعی سرور میدهد؛ اگر فقط متن ثابت صفحه عوض شد، در Network درخواست POST را پیدا کنید. ۳. نام خالی باید پیام قابل فهم بگیرد و ورودی پاک نشود؛ اعتبارسنجی فرم و سرور هر دو لازماند. ۴. متن نامعتبر با Content-Type JSON باید ۴۰۰ شود؛ صحیحبودن JSON با معتبربودن فیلدها یکسان نیست. ۵. فیلد اضافه یا نوع عددی نام، قرارداد API را با ۴۲۲ رد میکند. پاسخ هر مورد را با کد وضعیت و بدنه ثبت کنید.
۶. بدنهٔ بیشتر از حد برنامه باید رد شود؛ از دادهٔ ساختگی و حجم کوچک آزمون استفاده کنید. ۷. GET به /api/greet باید ۴۰۵ و روش مجاز بدهد، چون این مسیر POST میخواهد. ۸. مسیر ناشناخته ۴۰۴ است و نباید محتوی فایل دلخواه سیستم را بدهد. ۹. در انتشار عمومی، نشانی صفحه و درخواست API هر دو HTTPS همان دامنهاند؛ اگر گواهی منقضی یا نام نادرست است، صدور و نصب را اصلاح کنید. ۱۰. پس از توقف سرویس، مسیر عمومی ممکن است ۵۰۲ شود؛ به وضعیت سرویس و لاگ Nginx برگردید. این پاسخها شما را به لایهٔ مسئول هدایت میکنند و جای حدسزدن و نصب مجدد همهٔ ابزارها را میگیرند.
یک تحویل خوب شامل کد کامل، دستور اجرای دقیق، نام نسخهٔ Node، نشانی آزمایش، نتیجهٔ این سناریوها و توضیح انتخاب TLS است. هیچ کلید خصوصی، توکن یا نام واقعی کاربر را داخل گزارش نگذارید. این پروژه عمداً کوچک و بدون ذخیرهسازی است؛ برای فرم پرداخت، حساب کاربری یا نگهداری پیامها باید قرارداد داده، مجوز، ذخیرهسازی پایدار و کنترل سوءاستفاده جدا طراحی شود. هدف شما اکنون ساخت یک چرخهٔ کامل و قابل توضیح از صفحه تا درخواست، پاسخ و اتصال معتبر است؛ پس از تحویل میتوانید یک تغییر کوچک مثل متن پیام یا رنگ صفحه را با همین آزمونها توسعه دهید.
منابع رسمی انگلیسی
Node.js — HTTPS server: https://nodejs.org/api/https.html
Node.js — HTTP server lifecycle: https://nodejs.org/api/http.html
OpenSSL — local certificate extensions: https://docs.openssl.org/3.0/man1/openssl-req/
curl — certificate verification and custom trust: https://curl.se/docs/sslcerts.html
Nginx — HTTPS servers: https://nginx.org/en/docs/http/configuring_https_servers.html
تألیف و سناریوهای اختصاصی: تحریریه درسآوند.
متن درسهای عمومی را بدون ورود بخوانید.