простой экспресс-прокси – клиент получает HTTP-заголовок ответа дважды

Вопрос или проблема

Я попытался реализовать простой HTTP-прокси. Маршрут express /proxy/* должен перенаправить все запросы на сокет Unix. Проблема, с которой я столкнулся, заключается в том, что клиент/curl получает HTTP-ответ дважды. Один от прокси-сервера, другой от целевого прокси (часть тела).

curl -v http://localhost:8080/proxy/
* обрабатывается: http://localhost:8080/proxy/
*   Попытка подключения ::1:8080...
* подключение к ::1 порт 8080 завершилось неудачей: Вербиндунгсауфбау отклонён
*   Попытка подключения 127.0.0.1:8080...
* Подключено к localhost (127.0.0.1) порт 8080
> GET /proxy/ HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.2.1
> Accept: */*
> 
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Date: Вс, 29 Сен 2024 22:49:48 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
< Transfer-Encoding: chunked
< 
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 30
ETag: W/"1e-HL4/BzTDxxZMLqCN2LK+rorlmRE"
Date: Вс, 29 Сен 2024 22:49:48 GMT
Connection: keep-alive
Keep-Alive: timeout=5

* передача завершена с оставшимися данными для чтения
* Закрытие соединения
curl: (18) передача завершена с оставшимися данными для чтения
{"hello":"От сервера плагина"}

Как видно в выводе curl, второй HTTP-ответ заголовка является частью тела, что неправильно.

Код очень простой.

Прокси-сервер:

const express = require("express");
const {createConnection} = require("net");

const app = express();

app.all("/proxy/*", (req, res) => {

    const targetPath = req.url.replace(`/proxy/`, "https://stackoverflow.com/");
    const { method, httpVersion, headers } = req;

    const client = createConnection("/tmp/66e5934cead5404f0e23dfb1.sock", () => {

        client.write(`${method} ${targetPath} HTTP/${httpVersion}\r\n`);

        /*
        Object.entries(headers).forEach(([key, value]) => {
            client.write(`${key}: ${value}\r\n`);
        });
        */

        client.write(`\r\n\r\n`);

        client.pipe(res.socket);
        res.socket.pipe(client);

    });

    // имитация завершения
    // принудительное завершение запроса/ответа
    setTimeout(() => {
        res.socket.end();
    }, 3000);

});

app.listen(8080, "127.0.0.1", (err) => {
    console.log(err || "Ожидание запросов на 8080");
});

Целевой прокси, который слушает на доменном сокете:

const {createServer} = require("http");
const express = require("express");
const {tmpdir} = require("os");
const { rmSync } = require("fs");

const SOCKET_PATH = `${tmpdir()}/66e5934cead5404f0e23dfb1.sock`;

rmSync(SOCKET_PATH, {
    force: true
});

const app = express();
const server = createServer(app);

app.use((req, res, next) => {

    console.log("Запрос к серверу", req.method, req.url);
    //req.pipe(process.stdout);
    next();

});

app.get("/", (req, res) => {
    res.json({
        hello: "От сервера плагина"
    });
});

server.listen(SOCKET_PATH, (err) => {
    console.log(err || "сервер слушает", SOCKET_PATH);
});

Откуда берется первый заголовок ответа в curl?
Я не отправляю никакой ответ в обработчике маршрута express клиенту.

Все каналы 1:1 подключены к доменному сокету, и я предполагаю, что это единственная часть, которая отправляет HTTP-ответ.

Ответ или решение

Ваша проблема связана с тем, что вы отправляете данные от прокси-сервера к клиенту и от целевого сервера через один и тот же поток. Это вызывает дублирование заголовков ответа, потому что каждый из них отправляет свои собственные заголовки в ответ на HTTP-запрос.

Давайте разберем ваш код и предложим решение.

Проблема

Когда вы вызываете client.pipe(res.socket), вы фактически перенаправляете данные из сокета клиента в сокет proxy, что приводит к отправке заголовков и тела ответа дважды. Сначала из Express-прокси, а затем из целевого сервера.

Решение

Чтобы избежать дублирования заголовков, вам нужно убедиться, что вы не создаете заголовок в ответе, когда прокси-сервер уже получает данные от прокси-цели. Вместо этого необходимо перенаправить заголовки и тело ответа только от целевого сервера к клиенту.

Вот исправленный код для сервера-прокси, который должен решить вашу проблему:

const express = require("express");
const { createConnection } = require("net");

const app = express();

app.all("/proxy/*", (req, res) => {
    const targetPath = req.url.replace(`/proxy/`, "https://stackoverflow.com/");
    const { method, httpVersion, headers } = req;

    const client = createConnection("/tmp/66e5934cead5404f0e23dfb1.sock", () => {
        client.write(`${method} ${targetPath} HTTP/${httpVersion}\r\n`);

        // Копирование заголовков от клиента к целевому серверу
        Object.entries(headers).forEach(([key, value]) => {
            client.write(`${key}: ${value}\r\n`);
        });

        client.write(`\r\n`); // Завершение заголовков

        // Прокси-ответ от целевого сервера к клиенту
        client.on('data', (data) => {
            // Проверяем, является ли это заголовком
            if (!res.headersSent) {
                const [header, ...body] = data.toString().split('\r\n\r\n');
                const headers = header.split('\r\n');

                // Обработка заголовков
                headers.forEach((line) => {
                    if (!line.startsWith('HTTP/')) {
                        const [key, value] = line.split(': ');
                        res.setHeader(key, value);
                    }
                });

                res.writeHead(200);
                res.write(body.join('\r\n\r\n')); // Записываем тело ответа
                res.end();
            }
        });
    });

    client.on('error', (err) => {
        console.error('Socket error:', err);
        res.status(500).send('Сервер временно недоступен.');
    });
});

app.listen(8080, "127.0.0.1", (err) => {
    console.log(err || "Слушаем запросы на 8080");
});

Основные изменения:

  1. Перенаправление заголовков: Мы копируем заголовки из ответа целевого сервера только один раз, устанавливая их в ответе res прокси.

  2. Снижение повторного отправления заголовков: Мы контролируем, были ли уже отправлены заголовки с помощью res.headersSent.

Заключение

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

Оцените материал
Добавить комментарий

Капча загружается...