Repository navigation
Expand file tree
/
Copy pathindex.js
More file actions
140 lines (124 loc) · 5.99 KB
/
Copy pathindex.js
File metadata and controls
140 lines (124 loc) · 5.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
'use strict';
/**
* Provodox Lite — демонстрационный stateless reverse-proxy к MAX Bot API
* для Yandex Cloud Functions.
*
* Зачем: platform-api2.max.ru подписан корнем Минцифры, которому доверяет не каждая
* среда исполнения (Google Apps Script и др.) — и добавить этот корень в такой среде
* нельзя, прямой вызов падает с SSL Error. Provodox принимает запрос по доверенному
* всем сертификату (домен Yandex API Gateway *.apigw.yandexcloud.net выдан GlobalSign)
* и прозрачно форвардит его на апстрим, намеренно не проверяя цепочку сертификата
* апстрима — именно её клиент и не может проверить сам.
*
* Токен MAX (заголовок Authorization) прокси НЕ хранит: он пробрасывается от вызывающего
* как есть, авторизацию делает сам MAX.
*
* Своего контроля доступа у Lite нет намеренно: доступ — это знание адреса шлюза.
* Для личного контура такой модели достаточно, но адрес нельзя публиковать: кто его
* знает, тот ходит в MAX за ваш счёт. Где адрес не удержать в секрете — нужен контроль
* доступа (см. README).
*/
const https = require('https');
// Апстрим задан здесь намеренно: один сценарий, ноль конфигурации.
// Улучшение: вынести в переменную окружения — тогда апстрим меняется без правки кода.
const UPSTREAM_HOST = 'platform-api2.max.ru';
// Намеренно не проверяем цепочку сертификата апстрима (см. шапку файла).
const insecureAgent = new https.Agent({ rejectUnauthorized: false, keepAlive: true });
// Заголовки, которые не пробрасываем на апстрим (hop-by-hop и служебные шлюза).
const DROP_REQ_HEADERS = new Set([
'host', 'content-length', 'connection', 'keep-alive', 'proxy-authorization',
'te', 'trailer', 'transfer-encoding', 'upgrade',
'x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'x-real-ip',
'x-request-id', 'x-trace-id',
]);
// Заголовки, которые не возвращаем клиенту (рантайм/шлюз выставят свои).
const DROP_RES_HEADERS = new Set(['transfer-encoding', 'connection', 'content-length']);
/** Путь запроса: под API Gateway {proxy+} это event.url; при прямом вызове — event.path. */
function targetPath(event) {
let p =
event.url ||
event.path ||
(event.pathParameters && (event.pathParameters.proxy || event.pathParameters.path)) ||
'/';
// event.url может прийти уже с query — отрезаем, query собираем отдельно.
const q = p.indexOf('?');
if (q !== -1) p = p.slice(0, q);
if (!p.startsWith('/')) p = '/' + p;
return p;
}
/** Query-строка из multiValue (приоритет) либо single-value параметров события. */
function targetQuery(event) {
const usp = new URLSearchParams();
const mv = event.multiValueQueryStringParameters;
const sv = event.queryStringParameters;
if (mv && Object.keys(mv).length) {
for (const [k, vals] of Object.entries(mv)) for (const v of vals) usp.append(k, v);
} else if (sv) {
for (const [k, v] of Object.entries(sv)) usp.append(k, v);
}
const s = usp.toString();
return s ? '?' + s : '';
}
function forwardHeaders(event) {
const out = {};
for (const [k, v] of Object.entries(event.headers || {})) {
if (!DROP_REQ_HEADERS.has(k.toLowerCase())) out[k] = v;
}
out['Host'] = UPSTREAM_HOST;
return out;
}
function cleanResponseHeaders(h) {
const out = {};
for (const [k, v] of Object.entries(h || {})) {
if (!DROP_RES_HEADERS.has(k.toLowerCase())) out[k] = Array.isArray(v) ? v.join(', ') : v;
}
return out;
}
module.exports.handler = async (event) => {
// Один гард на входе: дальше forwardHeaders/targetPath безопасно работают с event,
// даже если рантайм вдруг передаст null/undefined.
event = event || {};
const method = (
event.httpMethod ||
(event.requestContext && event.requestContext.httpMethod) ||
'GET'
).toUpperCase();
const headers = forwardHeaders(event);
let bodyBuf = null;
if (event.body != null && event.body !== '') {
bodyBuf = Buffer.from(event.body, event.isBase64Encoded ? 'base64' : 'utf8');
headers['Content-Length'] = String(Buffer.byteLength(bodyBuf));
}
const options = {
host: UPSTREAM_HOST,
method,
path: targetPath(event) + targetQuery(event),
headers,
agent: insecureAgent,
};
return await new Promise((resolve) => {
const req = https.request(options, (res) => {
const chunks = [];
res.on('data', (c) => chunks.push(c));
res.on('end', () => {
resolve({
statusCode: res.statusCode,
headers: cleanResponseHeaders(res.headers),
// Возвращаем тело как двоичные данные (base64): корректно для JSON, gzip и вложений.
body: Buffer.concat(chunks).toString('base64'),
isBase64Encoded: true,
});
});
});
req.on('error', (err) => {
resolve({
statusCode: 502,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error: 'provodox_upstream_error', detail: String((err && err.message) || err) }),
isBase64Encoded: false,
});
});
if (bodyBuf) req.write(bodyBuf);
req.end();
});
};