Вопрос или проблема
Я попытался реализовать простой 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");
});
Основные изменения:
-
Перенаправление заголовков: Мы копируем заголовки из ответа целевого сервера только один раз, устанавливая их в ответе
res
прокси. -
Снижение повторного отправления заголовков: Мы контролируем, были ли уже отправлены заголовки с помощью
res.headersSent
.
Заключение
С помощью этого кода дублирование заголовков должно исчезнуть, и клиент получит корректный ответ. Убедитесь, что вы корректно обрабатываете ошибки соединения, чтобы пользователю было понятно, если что-то пойдет не так.