TeqFW: Web server

Дата публикации: 2021-12-15

Web-сервер на платформе TeqFW может работать в одном из трёх режимов: HTTP/1, HTTP/2 и HTTPS (over HTTP/2). Первый режим (HTTP/1) применяется для разработки — запуск приложения локально и доступ к нему из браузера напрямую, второй — для работы в связке с другими серверами (например, доступ через nginx-прокси), третий — в качестве отдельного сервера в live-режиме.

Основы HTTP

HTTP всегда работает по схеме “запросответ”. Вначале отправляется стартовая строка с адресом ресурса на сервере, за ней следуют HTTP-заголовки, потом — тело запроса (опционально). При ответе сервер сначала посылает HTTP-заголовки, а затем — тело ответа. Тело ответа может быть растянуто во времени (как в Server Sent Events — заголовки, событие, событие, …).

В nodejs есть две библиотеки для запуска web-сервера: http и http2. Сервера в обеих библиотеках построены по общему принципу — на каждый HTTP-запрос генерируется ряд событий, на которые можно повесить собственные функции-обработчики (listeners, “прослушки”):

server.on(‘connect’, onConnect);
server.on(‘error’, onError);
server.on(‘request’, onRequest);
server.on(‘close’, onClose);

… HTTP-сервера на каждый запрос формируют структуры для работы с входными и выходными данными и предоставляют разработчику возможность прослушивать событие request для доступа к ним:

server.on(‘request’, (req, res) => {}); Структуры req и res являются также потоками, за чтение/запись данных в которые отвечает прослушка (listener), т.е. — разработчик приложения.

Важным является то, что сообщения в HTTP состоят из двух частей: заголовков и тела. Все заголовки всегда отправляются перед телом (после начала отправки тела запроса/ответа заголовки отправлять более нельзя).

Основы обработки запроса в TeqFW

Прослушка события request выполняет предварительный анализ поступившего запроса, оставляя только те запросы, которые соответствуют допустимым HTTP-методам (для TeqFW — HEAD, GET & POST). На все другие запросы прослушка возвращает код 405 (Method Not Allowed).

Для POST-запросов, у которых Content-Type начинается с text/plain или с application/json, прослушка считывает тело запроса и преобразует его в текст или в JSON-объект, который затем прикрепляется к структуре req. Во всех остальных случаях прослушка входной поток не трогает.

Разработчики teq-плагинов могут создавать собственные обработчики web-запросов, которые request-прослушка использует для обработки поступившего запроса и формирования ответа. Обработчики по очереди анализируют запрос и, если запрос находится в их зоне ответственности, выполняют соответствующие действия. Если в результате этих действий формируются HTTP-заголовки для их отправки клиенту, то они добавляются к структуре res (через response.setHeader(name, value)). Если обработчик считает, что ответом должно быть содержимое некоего файла, то он указывает имя файла в res[‘teqFile’], если ответом должен быть текст, то он указывается в res[‘teqBody’].

Последним request-прослушка запускает обработчик TeqFw_Web_Back_Handler_Final, который анализирует res-структуру и формирует ответ (сначала все заголовки, затем отдаёт файл или текст тела сообщения). Если ни один из обработчиков запрос не обработал, то финальный обработчик возвращает код 404 (Not Found).

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

Диспетчер запросов

В TeqFW прослушка для request-события создаётся в модуле TeqFw_Web_Back_Server_Dispatcher (метод getListener()). Перед подключением прослушки Диспетчер сканирует все плагины в поисках обработчиков web-запросов и инициализирует их:

await dispatcher.createHandlers();
const onRequest = dispatcher.getListener();
server.on(‘request’, onRequest);

Обработчики запросов

Обработчик web-запросов — это объект, имплементирующий интерфейс TeqFw_Web_Back_Api_Request_IHandler:

class TeqFw_Web_Back_Api_Request_IHandler {
getProcessor() { }
async init() { }
requestIsMine({method, address, headers} = {}) {}
} Метод init() предназначен для асинхронной инициализации обработчика Диспетчером, метод requestIsMine() сообщает Диспетчеру, что обработчик признаёт запрос “своим”, метод getProcessor() возвращает Диспетчеру функцию — процессор запроса:
/**

*/

function process(req, res) {} Подключение обработчиков происходит в дескрипторе teq-плагина (teqfw.json):
{

@teqfw/web”: {

“handlers”: {

“TeqFw_Web_Back_Handler_Upload”: {

“after”: [“TeqFw_Web_Back_Handler_WAPI”],

“before”: [“TeqFw_Web_Back_Handler_Static”],

“space”: [“upload”]

}
}
}
}

Порядок подключения обработчиков определяется на основании данных в узлах after и before. Узел space позволяет обработчику зарезервировать адресное пространство для “своих” запросов (см. “Адресация ресурсов”).

Обработка запроса Диспетчером

При обработке HTTP-запроса Диспетчер проверяет метод запроса (HEAD, GET, POST) и считывает тело запроса (если тип содержимого text/plain или application/json). После чего проверяет очередь обработчиков и выбирает те, кто готов обработать запрос основываясь на данных заголовков и адресе запроса. Отобранные обработчики запускаются по-очереди в асинхронном режиме:

/** @type {TeqFw_Web_Back_Model_Address} */
const mAddress = spec[‘TeqFw_Web_Back_Model_Address$’];
/** @type {TeqFw_Web_Back_Server_Respond.respond405|function} */
const respond405 = spec[‘TeqFw_Web_Back_Server_Respond.respond405’];
/** @type {TeqFw_Web_Back_Api_Request_IHandler[]} */
const handlers = [];
async function onRequest(req, res) {
function isMethodAllowed(method) { }
async function parseBody(method, headers, req) { } const {headers, method, url} = req;
if (isMethodAllowed(method)) {
await parseBody(method, headers, req);
const address = mAddress.parsePath(url);
// collect processors
const active = [];
for (const one of handlers)
if (one.requestIsMine({method, address, headers}))
active.push(one.getProcessor());
// run processors one by one
for (const one of active)
await one(req, res);
} else respond405(res);
} В общем случае обработчик не должен сам отправлять ответ — ему неизвестно, какие ещё обработчики есть в очереди после него. Так как результатом обработки запроса является сообщение, состоящее из заголовков и тела запроса, то каждый обработчик может добавить свои заголовки в res (через response.setHeader(name, value)), а тело ответа или имя файла для отправки клиенту сохранить в res[‘teqBody’] или в res[‘teqFile’] соответственно. Если обработчик считает, что его данных достаточно для формирования ответа, он добавляет код ответа в res[‘teqStatus’].

Таким образом, каждый обработчик из очереди может поучаствовать в обработке запроса, добавить какие-либо данные в req-структуру (будут доступны для следующих обработчиков) или посмотреть в res-структуре, был ли запрос обработан предыдущими обработчиками и с каким результатом.

Финальный обработчик

В общем случае за формирование ответа на запрос отвечает финальный обработчик — TeqFw_Web_Back_Handler_Final. Он проверяет, что запрос не был обработан полностью каким-либо обработчиком (res.headersSent), смотрит, что нужно отправить в качестве ответа (файл или текст), формирует и отправляет ответ.

function process(req, res) {
if (!res.headersSent) {
const headers = res.getHeaders();
const statusCode = res[DEF.RES_STATUS] ?? HTTP_STATUS_OK;
const file = res[DEF.RES_FILE];
const body = res[DEF.RES_BODY];
let stat;
if (file) {
if (
existsSync(file)

&& (stat = statSync(file))

&& stat.isFile()

) {
// …
const readStream = createReadStream(file);
res.writeHead(statusCode, headers);
pipeline(readStream, res);
}
} else if (body) {
res.writeHead(statusCode, headers);
res.end(body);
} else respond404(res);
}
}

Если запрос вообще не был обработан ни одним обработчиком, то финальный обработчик отправляет клиенту ответ со статусом 404 (Not Found).

Доступные обработчики

На момент написания публикации в плагине @teqfw/web определены следующие обработчики:

Обработчики задают пространства адресов, очерчивая свою “зону ответственности” (sse, api, upload, src и web):

{

@teqfw/web”: {

“handlers”: {

“TeqFw_Web_Back_Handler_Static”: {

“spaces”: [“src”, “web”]

}
}
}
}

Модель адреса в платформе (TeqFw_Web_Back_Model_Address) способна определять, к какому пространству относится запрашиваемый ресурс:

/** @type {TeqFw_Web_Back_Model_Address} */
const mAddress = await container.get(‘TeqFw_Web_Back_Model_Address$’);
//…
const address = mAddress.parsePath(url);
if (address?.space === ‘api’) { /* … */}

Раздача статики

За раздачу статики отвечает обработчик TeqFw_Web_Back_Handler_Static. Вся статика делится на две группы:

Пространство ’src’

Каждый teq-плагин прописывает в дескрипторе ./teqfw.json настройки для автозагрузки кода контейнером объектов. Например, для teq-плагина с именем @vnd/pkg указана такая настройка автозагрузки:

{

@teqfw/di”: {

“autoload”: {

“ns”: “Vnd_Pkg”,

“path”: “./sources”
}
}
}

Обработчик статики использует эту настройку, чтобы в пространстве ./src/ предоставлять доступ к исходникам. Например, адрес:

https://host.com/src/@vnd/pkg/Path/To/Module.mjs соответствует такому пути к файлу внутри проекта:

./node_modules/@vnd/pkg/sources/Path/To/Module.mjs Если npm-пакет не является teq-плагином (нет дескриптора ./teqfw.json в корне пакета), то для таких пакетов маппинг исходников можно указать вручную в дескрипторе того плагина, который использует этот npm-пакет. Вот, например, маппинг статики для плагина @teqfw/vue :

{

@teqfw/web”: {

“statics”: {

“/vue-router/”: “/vue-router/dist/”,

“/vue/”: “/vue/dist/”
}
}
}

Таким образом, подобный маппинг позволяет перенаправить все адреса:

https://host.com/src/vue/… на файловую систему:

./node_modules/vue/dist/… ### Пространство ‘web’

Каждый teq-плагин может иметь в корне каталог ./web/, всё содержимое которого может быть доступным через web-сервер. Обработчик статики при инициализации сканирует все teq-плагины и составляет карту для доступа к таким ресурсам. Так, адрес:

https://host.com/web/@vnd/pkg/css/style.css соответствует такому пути к файлу внутри проекта:

./node_modules/@vnd/pkg/web/css/style.css

Web API (сервисы)

Обработчик TeqFw_Web_Back_Handler_WAPI отвечает за пространство ./api/ и реагирует на GET&POST методы. Если POST-запрос содержит какие-то данные, то ожидается, что это будет валидный JSON.

Сервисы внутри api-пространства адресуются маршрутами (routes, endpoints) без учёта метода запроса (один и тот же сервис и для GET, и для POST). Каждый teq-плагин определяет собственное подпространство в соответствии со своим npm-именем и уже внутри собственного подпространства определяет имя маршрута, по которому располагается соответствующий wapi-сервис:

http://localhost/api/@teqfw/web/load/namespaces Результатом работы сервиса также является JSON.

WAPI-сервисы teq-плагина регистрируются в его дескрипторе:

{

@teqfw/web”: {

“wapi”: [

“TeqFw_Web_Back_WAPI_Load_Config”,

“TeqFw_Web_Back_WAPI_Load_FilesToCache”,

“TeqFw_Web_Back_WAPI_Load_Namespaces”
]
}
}

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

Server Sent Events и Files Upload

Эти обработчики принципиально реализованы, но на данной стадии создавать описание было бы преждевременным. В SSE непонятен механизм группового использования канала связи с клиентом (когда различные teq-плагины пишут в канал свои события) — сейчас возможен только один сервис на канал. А в upload-обработчике пока непонятно, каким образом, без аутентификации и авторизации, обеспечить ограниченный доступ к загрузке файлов на сервер.

Поэтому для отключения обработчиков, которые не предполагается использовать в приложении, в дескрипторе ./teqfw.json приложения можно прописать:

{

@teqfw/web”: {

“excludes”: {

“handlers”: [“TeqFw_Web_Back_Handler_Upload”]

}
}
}

Резюме

Web-сервер платформы TeqFW может запускаться в одном из трёх режимов:

В нём реализованы обработка:

При необходимости возможно добавление дополнительных обработчиков (аутентификация, логирование и т.п.).