HTTPS в dev-сервере

По умолчанию большинство локальных серверов разработки запускаются по протоколу HTTP. Для многих задач этого достаточно, однако современные браузеры и веб-платформы всё чаще требуют защищённое соединение HTTPS даже на этапе локальной разработки.

Использование HTTPS в dev-сервере позволяет максимально приблизить локальное окружение к реальным условиям эксплуатации приложения и получить доступ к функциям браузера, которые работают только в защищённом контексте.

К таким возможностям относятся:

  • Service Workers;
  • Web Push Notifications;
  • Web Authentication (WebAuthn);
  • API доступа к камере и микрофону;
  • Geolocation API;
  • HTTP/2;
  • некоторые механизмы хранения данных и безопасности браузера;
  • тестирование Secure Cookies;
  • проверка политики Content Security Policy (CSP).

В проектах на Esbuild HTTPS настраивается через собственный сервер разработки либо через интеграцию с дополнительными инструментами.


Dev-сервер Esbuild и поддержка HTTPS

Esbuild предоставляет встроенный сервер разработки через метод serve(). Этот сервер предназначен прежде всего для быстрого локального тестирования результатов сборки.

Простейший запуск сервера выглядит следующим образом:

import * as esbuild from 'esbuild';

await esbuild.serve(
  {
    servedir: 'dist',
    port: 3000
  },
  {}
);

После запуска приложение будет доступно по адресу:

http://localhost:3000

Однако встроенный сервер Esbuild не предоставляет полноценного набора возможностей для настройки HTTPS, сравнимого с решениями вроде Vite, Webpack Dev Server или специализированных Node.js-серверов.

Поэтому в реальных проектах обычно используются дополнительные способы организации HTTPS.


Почему HTTPS важен даже локально

Современные браузеры рассматривают HTTPS не просто как механизм шифрования, а как показатель доверенного окружения.

Например, Service Worker не зарегистрируется при обычном HTTP-соединении:

navigator.serviceWorker.register('/sw.js');

При запуске через HTTP можно получить ошибку:

Failed to register a ServiceWorker:
The operation is insecure.

Аналогичная ситуация возникает с Web Push:

const subscription =
  await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: publicKey
  });

Без HTTPS подписка на push-уведомления работать не будет.


Использование самоподписанных сертификатов

Наиболее распространённый способ организации HTTPS в локальной среде — применение самоподписанных SSL-сертификатов.

Для генерации сертификата можно использовать OpenSSL.

Создание приватного ключа:

openssl genrsa -out localhost-key.pem 2048

Создание сертификата:

openssl req -new -x509 \
  -key localhost-key.pem \
  -out localhost.pem \
  -days 365

После выполнения команд будут получены два файла:

localhost-key.pem
localhost.pem

Первый содержит приватный ключ, второй — сертификат.


Создание собственного HTTPS-сервера поверх Esbuild

Поскольку встроенный сервер Esbuild имеет ограниченные возможности по работе с HTTPS, часто создаётся отдельный сервер на Node.js.

Сначала выполняется сборка:

import * as esbuild from 'esbuild';

await esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/app.js'
});

После этого запускается HTTPS-сервер.

Структура проекта:

project/
├─ src/
├─ dist/
├─ localhost.pem
├─ localhost-key.pem
└─ server.js

Пример сервера:

import fs from 'fs';
import https from 'https';
import express from 'express';

const app = express();

app.use(express.static('dist'));

https.createServer(
  {
    key: fs.readFileSync('localhost-key.pem'),
    cert: fs.readFileSync('localhost.pem')
  },
  app
).listen(3000);

Теперь приложение открывается по адресу:

https://localhost:3000

Автоматическая пересборка с HTTPS

Обычно HTTPS используется совместно с режимом наблюдения за файлами.

Esbuild предоставляет API Watch Mode.

import * as esbuild from 'esbuild';

const context = await esbuild.context({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/app.js'
});

await context.watch();

После изменения исходного кода пересборка происходит автоматически.

HTTPS-сервер при этом продолжает обслуживать обновлённые файлы.


Live Reload через HTTPS

Для повышения удобства разработки часто используется автоматическое обновление страницы.

Простейший вариант — WebSocket-соединение между браузером и сервером.

Клиентская часть:

const socket = new WebSocket(
  'wss://localhost:3001'
);

socket.addEventListener('message', () => {
  location.reload();
});

Обратите внимание на использование протокола:

wss://

Это защищённая версия WebSocket.

Если страница открыта через HTTPS, браузер может блокировать обычное соединение:

ws://

из-за политики смешанного контента (Mixed Content).


Mixed Content и его ограничения

Одной из наиболее распространённых проблем является смешивание защищённых и незащищённых ресурсов.

Например:

<script src="http://localhost:5000/app.js"></script>

Если сама страница открыта через HTTPS:

https://localhost:3000

браузер может заблокировать загрузку скрипта.

Ошибка выглядит примерно так:

Mixed Content:
The page was loaded over HTTPS,
but requested an insecure resource.

Поэтому все ресурсы должны использовать HTTPS:

<script src="https://localhost:5000/app.js"></script>

или относительные пути:

<script src="/app.js"></script>

Использование mkcert

Работа с самоподписанными сертификатами нередко вызывает предупреждения браузера.

Для решения этой проблемы широко применяется инструмент mkcert.

Он создаёт локальный центр сертификации и выпускает сертификаты, которым доверяет операционная система.

Установка:

mkcert -install

Создание сертификата:

mkcert localhost

Результат:

localhost.pem
localhost-key.pem

Преимущества такого подхода:

  • отсутствие предупреждений браузера;
  • автоматическое доверие сертификату;
  • поддержка нескольких доменных имён;
  • удобство командной разработки.

HTTPS для локальных доменов

Иногда требуется тестировать приложение не через localhost, а через отдельный домен.

Например:

app.local

или

frontend.local

Запись добавляется в файл hosts.

Linux и macOS:

/etc/hosts

Windows:

C:\Windows\System32\drivers\etc\hosts

Пример:

127.0.0.1 app.local

После этого можно выпустить сертификат:

mkcert app.local

И запускать сервер:

https://app.local:3000

Такой подход позволяет более точно воспроизводить производственное окружение.


HTTP/2 в локальной разработке

Большинство браузеров требуют HTTPS для работы HTTP/2.

При тестировании производительности может понадобиться именно этот протокол.

Пример запуска сервера:

import fs from 'fs';
import http2 from 'http2';

const server = http2.createSecureServer({
  key: fs.readFileSync('localhost-key.pem'),
  cert: fs.readFileSync('localhost.pem')
});

server.listen(3000);

Это позволяет исследовать:

  • мультиплексирование запросов;
  • приоритетизацию потоков;
  • особенности загрузки ресурсов;
  • сетевое поведение браузера.

Secure Cookies в Esbuild-проектах

Cookie с флагом Secure передаются только через HTTPS.

Пример установки:

res.cookie('session', token, {
  secure: true,
  httpOnly: true
});

При запуске приложения по HTTP такой cookie не будет работать корректно.

Локальное HTTPS позволяет полноценно тестировать:

  • авторизацию;
  • механизмы сессий;
  • JWT в cookie;
  • refresh token;
  • безопасность хранения данных.

Проверка Content Security Policy

Во многих приложениях используются строгие политики безопасности.

Пример заголовка:

Content-Security-Policy:
default-src 'self';

HTTPS помогает тестировать реальные сценарии:

res.setHeader(
  'Content-Security-Policy',
  "default-src 'self'"
);

Это особенно важно для:

  • корпоративных приложений;
  • финансовых сервисов;
  • административных панелей;
  • PWA-приложений.

Использование HTTPS совместно с проксированием API

Во время разработки фронтенд и бэкенд часто работают на разных портах.

Например:

Frontend:
https://localhost:3000

Backend:
https://localhost:5000

Для устранения проблем с CORS применяется проксирование.

Схема работы:

Browser
    |
    v
HTTPS Frontend
    |
    v
Proxy
    |
    v
Backend API

В результате браузер взаимодействует только с одним источником данных, а прокси перенаправляет запросы на сервер API.


Типичные ошибки при настройке HTTPS

Неверный путь к сертификату

cert: fs.readFileSync('./cert.pem')

Ошибка:

ENOENT

Причина — отсутствие файла по указанному пути.


Несоответствие домена сертификату

Сертификат выпущен для:

localhost

а приложение открывается через:

app.local

В этом случае браузер сообщает:

NET::ERR_CERT_COMMON_NAME_INVALID

Использование HTTP-ресурсов внутри HTTPS-страницы

Например:

<img src="http://localhost/image.png">

или:

fetch('http://localhost:5000/api');

Такие запросы могут блокироваться браузером.


Использование обычного WebSocket

Ошибка:

new WebSocket(
  'ws://localhost:3001'
);

при HTTPS-сайте.

Правильный вариант:

new WebSocket(
  'wss://localhost:3001'
);

Архитектура HTTPS-разработки с Esbuild

Наиболее распространённая схема выглядит следующим образом:

src
 │
 ▼
Esbuild Watch
 │
 ▼
dist
 │
 ▼
HTTPS Server
 │
 ▼
Browser

В этой архитектуре Esbuild отвечает исключительно за сборку и отслеживание изменений файлов, а HTTPS, WebSocket, проксирование запросов, HTTP/2 и прочие сетевые возможности реализуются отдельным сервером на Node.js. Такой подход обеспечивает максимальную гибкость, позволяет воспроизводить условия промышленной эксплуатации приложения и использовать все современные возможности браузеров, требующие защищённого соединения.