refactor: centralize HTTP proxy helpers and align IA_DEV_ROOT resolution (0.0.6)
Initial state: - HTTP proxy utilities (Bearer parsing, hop-by-hop headers, body limits, safe path validation) were duplicated between smart-ide-sso-gateway and smart-ide-global-api. - IA_DEV_ROOT auto-resolution order differed between bash (ensure-ia-dev-project-link.sh) and TypeScript (ia-dev-gateway getIaDevRoot), and could fall back to non-existing paths. Motivation: - Reduce duplication and drift across proxy layers. - Enforce consistent, explicit IA_DEV_ROOT behavior across scripts and services. Resolution: - Add package @4nk/smart-ide-http-utils and reuse it from smart-ide-sso-gateway and smart-ide-global-api. - Align IA_DEV_ROOT resolution to prefer ./services/ia_dev then ./ia_dev; fail fast when missing/misconfigured. Root cause: - Cross-service utilities were implemented ad-hoc in each service. - Historical layout transitions (ia_dev gitlink vs vendored services/ia_dev) left multiple resolvers with different priorities. Impacted features: - HTTP proxy chain (SSO gateway -> global API -> upstream services). - ia-dev-gateway startup/operation when IA_DEV_ROOT is missing or invalid. Code modified: - packages/smart-ide-http-utils/** - services/smart-ide-global-api/src/server.ts - services/smart-ide-sso-gateway/src/server.ts - services/ia-dev-gateway/src/paths.ts - scripts/ensure-ia-dev-project-link.sh Documentation modified: - docs/system-architecture.md - docs/ia_dev-module.md - docs/repo/README.md Configurations modified: - services/smart-ide-global-api/package.json - services/smart-ide-sso-gateway/package.json Files in deploy modified: - None Files in logs impacted: - None (runtime logs only) Databases and other sources modified: - None Off-project modifications: - None Files in .smartIde modified: - None Files in .secrets modified: - None New patch version in VERSION: - 0.0.6 CHANGELOG.md updated: - yes
This commit is contained in:
parent
58cc2493e5
commit
cfa1f435cb
12
CHANGELOG.md
12
CHANGELOG.md
@ -1,5 +1,17 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.0.6 - 2026-04-04
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `@4nk/smart-ide-http-utils`: shared HTTP proxy helpers (Bearer/body limit/hop-by-hop headers/safe proxy paths).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `smart-ide-sso-gateway` and `smart-ide-global-api`: reuse shared HTTP helpers (reduces duplication).
|
||||||
|
- `IA_DEV_ROOT` resolution: prefer `./services/ia_dev` then `./ia_dev` (code + docs); fail fast if missing in `ia-dev-gateway`.
|
||||||
|
- `scripts/ensure-ia-dev-project-link.sh`: prefer `services/ia_dev` when both layouts exist.
|
||||||
|
|
||||||
## 0.0.5 - 2026-04-04
|
## 0.0.5 - 2026-04-04
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@ -30,7 +30,7 @@ Le périmètre « service » côté monorepo est documenté sous [repo/ia-de
|
|||||||
| **smart_ide** | Cible UX IDE, scripts socle, systemd, doc de déploiement, **`logs/`** |
|
| **smart_ide** | Cible UX IDE, scripts socle, systemd, doc de déploiement, **`logs/`** |
|
||||||
| **`services/ia_dev/`** (module dans ce dépôt) | Agents, déploiements, ticketing ; confs projet dans `./projects/` + liens sous `services/ia_dev/projects/` |
|
| **`services/ia_dev/`** (module dans ce dépôt) | Agents, déploiements, ticketing ; confs projet dans `./projects/` + liens sous `services/ia_dev/projects/` |
|
||||||
|
|
||||||
Le futur **agent gateway** doit traiter `IA_DEV_ROOT` comme chemin canonique sur le serveur (résolution par défaut : `./ia_dev` puis `./services/ia_dev`). Voir [system-architecture.md](./system-architecture.md).
|
Le futur **agent gateway** doit traiter `IA_DEV_ROOT` comme chemin canonique sur le serveur (résolution par défaut : `./services/ia_dev` puis `./ia_dev`). Voir [system-architecture.md](./system-architecture.md).
|
||||||
|
|
||||||
## Trajectoire : service `ia-dev-gateway`
|
## Trajectoire : service `ia-dev-gateway`
|
||||||
|
|
||||||
|
|||||||
@ -45,6 +45,7 @@ Toute la documentation **opérationnelle** qui vivait auparavant sous des `READM
|
|||||||
| [service-smart-ide-global-api.md](./service-smart-ide-global-api.md) | API HTTP interne : proxy vers micro-services (Bearer partagé avec SSO) |
|
| [service-smart-ide-global-api.md](./service-smart-ide-global-api.md) | API HTTP interne : proxy vers micro-services (Bearer partagé avec SSO) |
|
||||||
| [service-smart-ide-sso-gateway.md](./service-smart-ide-sso-gateway.md) | Passerelle OIDC utilisateur → API globale → micro-services |
|
| [service-smart-ide-sso-gateway.md](./service-smart-ide-sso-gateway.md) | Passerelle OIDC utilisateur → API globale → micro-services |
|
||||||
| [../packages/smart-ide-upstreams/README.md](../packages/smart-ide-upstreams/README.md) | Paquet `@4nk/smart-ide-upstreams` : liste des clés et résolution des URL / jetons |
|
| [../packages/smart-ide-upstreams/README.md](../packages/smart-ide-upstreams/README.md) | Paquet `@4nk/smart-ide-upstreams` : liste des clés et résolution des URL / jetons |
|
||||||
|
| [../packages/smart-ide-http-utils/README.md](../packages/smart-ide-http-utils/README.md) | Paquet `@4nk/smart-ide-http-utils` : helpers HTTP partagés (proxy, headers, safe path) |
|
||||||
| [extension-anythingllm-workspaces.md](./extension-anythingllm-workspaces.md) | Extension AnythingLLM IDE (supprimée ; anythingllm-devtools) |
|
| [extension-anythingllm-workspaces.md](./extension-anythingllm-workspaces.md) | Extension AnythingLLM IDE (supprimée ; anythingllm-devtools) |
|
||||||
|
|
||||||
Les **spécifications** détaillées (contrats HTTP, sécurité, orchestration) restent dans [../API/README.md](../API/README.md) et [../features/](../features/).
|
Les **spécifications** détaillées (contrats HTTP, sécurité, orchestration) restent dans [../API/README.md](../API/README.md) et [../features/](../features/).
|
||||||
|
|||||||
@ -56,7 +56,7 @@ Chaque environnement possède ses **URLs**, **secrets** et **politiques** (Anyth
|
|||||||
|
|
||||||
## Module `ia_dev` dans ce dépôt
|
## Module `ia_dev` dans ce dépôt
|
||||||
|
|
||||||
Le répertoire **`./services/ia_dev`** fait partie du dépôt **smart_ide** (référence historique : [4nk/ia_dev](https://git.4nkweb.com/4nk/ia_dev.git) sur la forge). Sur le serveur SSH, l’**agent gateway** et les outils peuvent pointer vers `IA_DEV_ROOT` (résolution par défaut : `./ia_dev` puis `./services/ia_dev`) comme racine d’exécution des agents (scripts invoqués depuis la racine `ia_dev`). Voir [ia_dev-module.md](./ia_dev-module.md) et [repo/ia-dev-smart-ide-integration.md](./repo/ia-dev-smart-ide-integration.md).
|
Le répertoire **`./services/ia_dev`** fait partie du dépôt **smart_ide** (référence historique : [4nk/ia_dev](https://git.4nkweb.com/4nk/ia_dev.git) sur la forge). Sur le serveur SSH, l’**agent gateway** et les outils peuvent pointer vers `IA_DEV_ROOT` (résolution par défaut : `./services/ia_dev` puis `./ia_dev`) comme racine d’exécution des agents (scripts invoqués depuis la racine `ia_dev`). Voir [ia_dev-module.md](./ia_dev-module.md) et [repo/ia-dev-smart-ide-integration.md](./repo/ia-dev-smart-ide-integration.md).
|
||||||
|
|
||||||
## Répartition physique (première cible)
|
## Répartition physique (première cible)
|
||||||
|
|
||||||
|
|||||||
2
packages/smart-ide-http-utils/.gitignore
vendored
Normal file
2
packages/smart-ide-http-utils/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
|
||||||
28
packages/smart-ide-http-utils/README.md
Normal file
28
packages/smart-ide-http-utils/README.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# @4nk/smart-ide-http-utils
|
||||||
|
|
||||||
|
Utilitaires HTTP partagés pour les services Node/TypeScript du monorepo **smart_ide**.
|
||||||
|
|
||||||
|
Objectifs :
|
||||||
|
|
||||||
|
- Réduire la duplication (Bearer, limites de body, headers hop-by-hop, etc.).
|
||||||
|
- Centraliser les garde-fous de proxy (ex. rejet des segments `..` dans les chemins relayés).
|
||||||
|
|
||||||
|
Fonctions exposées :
|
||||||
|
|
||||||
|
- `readBearer(req)` : lit `Authorization: Bearer …`.
|
||||||
|
- `readBodyBuffer(req, maxBytes)` : lit un corps en mémoire avec plafond explicite.
|
||||||
|
- `copyHeadersForProxy(req)` : copie les headers d’entrée en excluant hop-by-hop + `Authorization`.
|
||||||
|
- `isSafeProxyPath(path)` : valide un chemin relayé (refuse `.` / `..` même encodés).
|
||||||
|
- `REQUEST_HOP_BY_HOP_HEADERS`, `RESPONSE_HOP_BY_HOP_HEADERS`.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Le répertoire **`dist/`** est versionné pour que les services puissent installer ce paquet via `file:` sans étape de build préalable.
|
||||||
|
|
||||||
|
Après modification de `src/` :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd packages/smart-ide-http-utils
|
||||||
|
npm ci && npm run build
|
||||||
|
```
|
||||||
|
|
||||||
2
packages/smart-ide-http-utils/dist/index.d.ts
vendored
Normal file
2
packages/smart-ide-http-utils/dist/index.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { REQUEST_HOP_BY_HOP_HEADERS, RESPONSE_HOP_BY_HOP_HEADERS, readBearer, readBodyBuffer, copyHeadersForProxy, isSafeProxyPath, } from "./proxy.js";
|
||||||
|
//# sourceMappingURL=index.d.ts.map
|
||||||
1
packages/smart-ide-http-utils/dist/index.d.ts.map
vendored
Normal file
1
packages/smart-ide-http-utils/dist/index.d.ts.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,0BAA0B,EAC1B,2BAA2B,EAC3B,UAAU,EACV,cAAc,EACd,mBAAmB,EACnB,eAAe,GAChB,MAAM,YAAY,CAAC"}
|
||||||
1
packages/smart-ide-http-utils/dist/index.js
vendored
Normal file
1
packages/smart-ide-http-utils/dist/index.js
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { REQUEST_HOP_BY_HOP_HEADERS, RESPONSE_HOP_BY_HOP_HEADERS, readBearer, readBodyBuffer, copyHeadersForProxy, isSafeProxyPath, } from "./proxy.js";
|
||||||
10
packages/smart-ide-http-utils/dist/proxy.d.ts
vendored
Normal file
10
packages/smart-ide-http-utils/dist/proxy.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import type * as http from "node:http";
|
||||||
|
export declare const REQUEST_HOP_BY_HOP_HEADERS: Set<string>;
|
||||||
|
export declare const RESPONSE_HOP_BY_HOP_HEADERS: Set<string>;
|
||||||
|
export declare const readBearer: (req: http.IncomingMessage) => string | null;
|
||||||
|
export declare const readBodyBuffer: (req: http.IncomingMessage, maxBytes: number) => Promise<Buffer>;
|
||||||
|
export declare const copyHeadersForProxy: (req: http.IncomingMessage, opts?: {
|
||||||
|
skipLowercase?: ReadonlySet<string>;
|
||||||
|
}) => Headers;
|
||||||
|
export declare const isSafeProxyPath: (p: string) => boolean;
|
||||||
|
//# sourceMappingURL=proxy.d.ts.map
|
||||||
1
packages/smart-ide-http-utils/dist/proxy.d.ts.map
vendored
Normal file
1
packages/smart-ide-http-utils/dist/proxy.d.ts.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AAEvC,eAAO,MAAM,0BAA0B,aAUrC,CAAC;AAEH,eAAO,MAAM,2BAA2B,aAKtC,CAAC;AAEH,eAAO,MAAM,UAAU,GAAI,KAAK,IAAI,CAAC,eAAe,KAAG,MAAM,GAAG,IAI/D,CAAC;AAEF,eAAO,MAAM,cAAc,GACzB,KAAK,IAAI,CAAC,eAAe,EACzB,UAAU,MAAM,KACf,OAAO,CAAC,MAAM,CAYhB,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAC9B,KAAK,IAAI,CAAC,eAAe,EACzB,OAAO;IAAE,aAAa,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CAAE,KAC7C,OAmBF,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,GAAG,MAAM,KAAG,OAyB3C,CAAC"}
|
||||||
82
packages/smart-ide-http-utils/dist/proxy.js
vendored
Normal file
82
packages/smart-ide-http-utils/dist/proxy.js
vendored
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
export const REQUEST_HOP_BY_HOP_HEADERS = new Set([
|
||||||
|
"connection",
|
||||||
|
"keep-alive",
|
||||||
|
"proxy-authenticate",
|
||||||
|
"proxy-authorization",
|
||||||
|
"te",
|
||||||
|
"trailers",
|
||||||
|
"transfer-encoding",
|
||||||
|
"upgrade",
|
||||||
|
"host",
|
||||||
|
]);
|
||||||
|
export const RESPONSE_HOP_BY_HOP_HEADERS = new Set([
|
||||||
|
"connection",
|
||||||
|
"keep-alive",
|
||||||
|
"transfer-encoding",
|
||||||
|
"content-encoding",
|
||||||
|
]);
|
||||||
|
export const readBearer = (req) => {
|
||||||
|
const raw = req.headers.authorization ?? "";
|
||||||
|
const m = /^Bearer\s+(.+)$/i.exec(raw);
|
||||||
|
return m?.[1]?.trim() ?? null;
|
||||||
|
};
|
||||||
|
export const readBodyBuffer = async (req, maxBytes) => {
|
||||||
|
const chunks = [];
|
||||||
|
let total = 0;
|
||||||
|
for await (const chunk of req) {
|
||||||
|
const b = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
||||||
|
total += b.length;
|
||||||
|
if (total > maxBytes) {
|
||||||
|
throw new Error(`Request body exceeds ${maxBytes} bytes`);
|
||||||
|
}
|
||||||
|
chunks.push(b);
|
||||||
|
}
|
||||||
|
return Buffer.concat(chunks);
|
||||||
|
};
|
||||||
|
export const copyHeadersForProxy = (req, opts) => {
|
||||||
|
const out = new Headers();
|
||||||
|
for (const [k, v] of Object.entries(req.headers)) {
|
||||||
|
if (!v) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const lk = k.toLowerCase();
|
||||||
|
if (REQUEST_HOP_BY_HOP_HEADERS.has(lk)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (lk === "authorization") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (opts?.skipLowercase?.has(lk)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.set(k, Array.isArray(v) ? v.join(", ") : v);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
export const isSafeProxyPath = (p) => {
|
||||||
|
if (!p.startsWith("/")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const rawSeg of p.split("/")) {
|
||||||
|
if (rawSeg.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (rawSeg === "." || rawSeg === "..") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let seg;
|
||||||
|
try {
|
||||||
|
seg = decodeURIComponent(rawSeg);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (seg === "." || seg === "..") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (seg.includes("/") || seg.includes("\\")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
51
packages/smart-ide-http-utils/package-lock.json
generated
Normal file
51
packages/smart-ide-http-utils/package-lock.json
generated
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"name": "@4nk/smart-ide-http-utils",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "@4nk/smart-ide-http-utils",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.11.0",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "20.19.39",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz",
|
||||||
|
"integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
packages/smart-ide-http-utils/package.json
Normal file
27
packages/smart-ide-http-utils/package.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "@4nk/smart-ide-http-utils",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Shared HTTP helpers for smart_ide Node services (proxy headers, body limits, safe paths).",
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p ."
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.11.0",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
9
packages/smart-ide-http-utils/src/index.ts
Normal file
9
packages/smart-ide-http-utils/src/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export {
|
||||||
|
REQUEST_HOP_BY_HOP_HEADERS,
|
||||||
|
RESPONSE_HOP_BY_HOP_HEADERS,
|
||||||
|
readBearer,
|
||||||
|
readBodyBuffer,
|
||||||
|
copyHeadersForProxy,
|
||||||
|
isSafeProxyPath,
|
||||||
|
} from "./proxy.js";
|
||||||
|
|
||||||
95
packages/smart-ide-http-utils/src/proxy.ts
Normal file
95
packages/smart-ide-http-utils/src/proxy.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import type * as http from "node:http";
|
||||||
|
|
||||||
|
export const REQUEST_HOP_BY_HOP_HEADERS = new Set([
|
||||||
|
"connection",
|
||||||
|
"keep-alive",
|
||||||
|
"proxy-authenticate",
|
||||||
|
"proxy-authorization",
|
||||||
|
"te",
|
||||||
|
"trailers",
|
||||||
|
"transfer-encoding",
|
||||||
|
"upgrade",
|
||||||
|
"host",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const RESPONSE_HOP_BY_HOP_HEADERS = new Set([
|
||||||
|
"connection",
|
||||||
|
"keep-alive",
|
||||||
|
"transfer-encoding",
|
||||||
|
"content-encoding",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const readBearer = (req: http.IncomingMessage): string | null => {
|
||||||
|
const raw = req.headers.authorization ?? "";
|
||||||
|
const m = /^Bearer\s+(.+)$/i.exec(raw);
|
||||||
|
return m?.[1]?.trim() ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readBodyBuffer = async (
|
||||||
|
req: http.IncomingMessage,
|
||||||
|
maxBytes: number,
|
||||||
|
): Promise<Buffer> => {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
let total = 0;
|
||||||
|
for await (const chunk of req) {
|
||||||
|
const b = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
||||||
|
total += b.length;
|
||||||
|
if (total > maxBytes) {
|
||||||
|
throw new Error(`Request body exceeds ${maxBytes} bytes`);
|
||||||
|
}
|
||||||
|
chunks.push(b);
|
||||||
|
}
|
||||||
|
return Buffer.concat(chunks);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const copyHeadersForProxy = (
|
||||||
|
req: http.IncomingMessage,
|
||||||
|
opts?: { skipLowercase?: ReadonlySet<string> },
|
||||||
|
): Headers => {
|
||||||
|
const out = new Headers();
|
||||||
|
for (const [k, v] of Object.entries(req.headers)) {
|
||||||
|
if (!v) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const lk = k.toLowerCase();
|
||||||
|
if (REQUEST_HOP_BY_HOP_HEADERS.has(lk)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (lk === "authorization") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (opts?.skipLowercase?.has(lk)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.set(k, Array.isArray(v) ? v.join(", ") : v);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isSafeProxyPath = (p: string): boolean => {
|
||||||
|
if (!p.startsWith("/")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const rawSeg of p.split("/")) {
|
||||||
|
if (rawSeg.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (rawSeg === "." || rawSeg === "..") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let seg: string;
|
||||||
|
try {
|
||||||
|
seg = decodeURIComponent(rawSeg);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (seg === "." || seg === "..") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (seg.includes("/") || seg.includes("\\")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
18
packages/smart-ide-http-utils/tsconfig.json
Normal file
18
packages/smart-ide-http-utils/tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"]
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Create ia_dev/projects/<id> -> ../../projects/<id> so ia_dev scripts resolve conf.json
|
# Create IA_DEV_ROOT/projects/<id> -> ../../projects/<id> so ia_dev scripts resolve conf.json
|
||||||
# from the monorepo versioned projects/<id>/.
|
# from the monorepo versioned projects/<id>/.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
@ -12,12 +12,12 @@ if [[ ! -f "${CONF}" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
IA_DEV_DIR=""
|
IA_DEV_DIR=""
|
||||||
if [[ -d "${ROOT}/ia_dev" ]]; then
|
if [[ -d "${ROOT}/services/ia_dev" ]]; then
|
||||||
IA_DEV_DIR="${ROOT}/ia_dev"
|
|
||||||
elif [[ -d "${ROOT}/services/ia_dev" ]]; then
|
|
||||||
IA_DEV_DIR="${ROOT}/services/ia_dev"
|
IA_DEV_DIR="${ROOT}/services/ia_dev"
|
||||||
|
elif [[ -d "${ROOT}/ia_dev" ]]; then
|
||||||
|
IA_DEV_DIR="${ROOT}/ia_dev"
|
||||||
else
|
else
|
||||||
echo "Missing ia_dev directory: expected '${ROOT}/ia_dev' or '${ROOT}/services/ia_dev'" >&2
|
echo "Missing ia_dev directory: expected '${ROOT}/services/ia_dev' or '${ROOT}/ia_dev'" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@ -12,24 +12,32 @@ export const dirExists = (p: string): boolean => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Path to ia_dev checkout: IA_DEV_ROOT env or monorepo ./ia_dev or ./services/ia_dev */
|
/** Path to ia_dev checkout: IA_DEV_ROOT env or monorepo ./services/ia_dev or ./ia_dev */
|
||||||
export const getIaDevRoot = (): string => {
|
export const getIaDevRoot = (): string => {
|
||||||
const fromEnv = process.env.IA_DEV_ROOT?.trim();
|
const fromEnv = process.env.IA_DEV_ROOT?.trim();
|
||||||
if (fromEnv && fromEnv.length > 0) {
|
if (fromEnv && fromEnv.length > 0) {
|
||||||
return path.resolve(fromEnv);
|
const resolved = path.resolve(fromEnv);
|
||||||
|
if (!dirExists(resolved)) {
|
||||||
|
throw new Error(`IA_DEV_ROOT is set but is not a directory: ${resolved}`);
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
const monorepoRoot = path.resolve(__dirname, "..", "..", "..");
|
const monorepoRoot = path.resolve(__dirname, "..", "..", "..");
|
||||||
const candidates = [
|
const candidates = [
|
||||||
path.join(monorepoRoot, "ia_dev"),
|
|
||||||
path.join(monorepoRoot, "services", "ia_dev"),
|
path.join(monorepoRoot, "services", "ia_dev"),
|
||||||
|
path.join(monorepoRoot, "ia_dev"),
|
||||||
];
|
];
|
||||||
for (const c of candidates) {
|
for (const c of candidates) {
|
||||||
if (dirExists(c)) {
|
if (dirExists(c)) {
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return candidates[0];
|
throw new Error(
|
||||||
|
`Missing ia_dev directory. Set IA_DEV_ROOT or create one of: ${candidates
|
||||||
|
.map((c) => JSON.stringify(c))
|
||||||
|
.join(", ")}`,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const agentsDir = (iaDevRoot: string): string =>
|
export const agentsDir = (iaDevRoot: string): string =>
|
||||||
|
|||||||
17
services/smart-ide-global-api/package-lock.json
generated
17
services/smart-ide-global-api/package-lock.json
generated
@ -9,6 +9,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@4nk/smart-ide-http-utils": "file:../../packages/smart-ide-http-utils",
|
||||||
"@4nk/smart-ide-upstreams": "file:../../packages/smart-ide-upstreams"
|
"@4nk/smart-ide-upstreams": "file:../../packages/smart-ide-upstreams"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -19,6 +20,18 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"../../packages/smart-ide-http-utils": {
|
||||||
|
"name": "@4nk/smart-ide-http-utils",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.11.0",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"../../packages/smart-ide-upstreams": {
|
"../../packages/smart-ide-upstreams": {
|
||||||
"name": "@4nk/smart-ide-upstreams",
|
"name": "@4nk/smart-ide-upstreams",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
@ -31,6 +44,10 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@4nk/smart-ide-http-utils": {
|
||||||
|
"resolved": "../../packages/smart-ide-http-utils",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@4nk/smart-ide-upstreams": {
|
"node_modules/@4nk/smart-ide-upstreams": {
|
||||||
"resolved": "../../packages/smart-ide-upstreams",
|
"resolved": "../../packages/smart-ide-upstreams",
|
||||||
"link": true
|
"link": true
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@4nk/smart-ide-http-utils": "file:../../packages/smart-ide-http-utils",
|
||||||
"@4nk/smart-ide-upstreams": "file:../../packages/smart-ide-upstreams"
|
"@4nk/smart-ide-upstreams": "file:../../packages/smart-ide-upstreams"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -4,6 +4,13 @@ import {
|
|||||||
resolveUpstream,
|
resolveUpstream,
|
||||||
type UpstreamAuth,
|
type UpstreamAuth,
|
||||||
} from "@4nk/smart-ide-upstreams";
|
} from "@4nk/smart-ide-upstreams";
|
||||||
|
import {
|
||||||
|
RESPONSE_HOP_BY_HOP_HEADERS,
|
||||||
|
copyHeadersForProxy,
|
||||||
|
isSafeProxyPath,
|
||||||
|
readBearer,
|
||||||
|
readBodyBuffer,
|
||||||
|
} from "@4nk/smart-ide-http-utils";
|
||||||
import { appendGlobalApiAccessLog } from "./accessLog.js";
|
import { appendGlobalApiAccessLog } from "./accessLog.js";
|
||||||
|
|
||||||
const HOST = process.env.GLOBAL_API_HOST ?? "127.0.0.1";
|
const HOST = process.env.GLOBAL_API_HOST ?? "127.0.0.1";
|
||||||
@ -17,80 +24,8 @@ const json = (res: http.ServerResponse, status: number, body: unknown): void =>
|
|||||||
res.end(JSON.stringify(body));
|
res.end(JSON.stringify(body));
|
||||||
};
|
};
|
||||||
|
|
||||||
const readBearer = (req: http.IncomingMessage): string | null => {
|
|
||||||
const raw = req.headers.authorization ?? "";
|
|
||||||
const m = /^Bearer\s+(.+)$/i.exec(raw);
|
|
||||||
return m?.[1]?.trim() ?? null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isSafeProxyPath = (p: string): boolean => {
|
|
||||||
if (!p.startsWith("/")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (const rawSeg of p.split("/")) {
|
|
||||||
if (rawSeg.length === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (rawSeg === "." || rawSeg === "..") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let seg: string;
|
|
||||||
try {
|
|
||||||
seg = decodeURIComponent(rawSeg);
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (seg === "." || seg === "..") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (seg.includes("/") || seg.includes("\\")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const readBodyBuffer = async (req: http.IncomingMessage): Promise<Buffer> => {
|
|
||||||
const chunks: Buffer[] = [];
|
|
||||||
let total = 0;
|
|
||||||
for await (const chunk of req) {
|
|
||||||
const b = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
||||||
total += b.length;
|
|
||||||
if (total > MAX_BODY_BYTES) {
|
|
||||||
throw new Error(`Request body exceeds ${MAX_BODY_BYTES} bytes`);
|
|
||||||
}
|
|
||||||
chunks.push(b);
|
|
||||||
}
|
|
||||||
return Buffer.concat(chunks);
|
|
||||||
};
|
|
||||||
|
|
||||||
const hopByHop = new Set([
|
|
||||||
"connection",
|
|
||||||
"keep-alive",
|
|
||||||
"proxy-authenticate",
|
|
||||||
"proxy-authorization",
|
|
||||||
"te",
|
|
||||||
"trailers",
|
|
||||||
"transfer-encoding",
|
|
||||||
"upgrade",
|
|
||||||
"host",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const buildForwardHeaders = (req: http.IncomingMessage, serviceAuth: UpstreamAuth): Headers => {
|
const buildForwardHeaders = (req: http.IncomingMessage, serviceAuth: UpstreamAuth): Headers => {
|
||||||
const out = new Headers();
|
const out = copyHeadersForProxy(req);
|
||||||
for (const [k, v] of Object.entries(req.headers)) {
|
|
||||||
if (!v) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const lk = k.toLowerCase();
|
|
||||||
if (hopByHop.has(lk)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (lk === "authorization") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
out.set(k, Array.isArray(v) ? v.join(", ") : v);
|
|
||||||
}
|
|
||||||
if (serviceAuth.kind === "bearer") {
|
if (serviceAuth.kind === "bearer") {
|
||||||
if (serviceAuth.token) {
|
if (serviceAuth.token) {
|
||||||
out.set("Authorization", `Bearer ${serviceAuth.token}`);
|
out.set("Authorization", `Bearer ${serviceAuth.token}`);
|
||||||
@ -101,13 +36,6 @@ const buildForwardHeaders = (req: http.IncomingMessage, serviceAuth: UpstreamAut
|
|||||||
return out;
|
return out;
|
||||||
};
|
};
|
||||||
|
|
||||||
const responseHopByHop = new Set([
|
|
||||||
"connection",
|
|
||||||
"keep-alive",
|
|
||||||
"transfer-encoding",
|
|
||||||
"content-encoding",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const proxyToUpstream = async (
|
const proxyToUpstream = async (
|
||||||
res: http.ServerResponse,
|
res: http.ServerResponse,
|
||||||
targetUrl: string,
|
targetUrl: string,
|
||||||
@ -126,7 +54,7 @@ const proxyToUpstream = async (
|
|||||||
const out = await fetch(targetUrl, init);
|
const out = await fetch(targetUrl, init);
|
||||||
res.statusCode = out.status;
|
res.statusCode = out.status;
|
||||||
for (const [k, v] of out.headers) {
|
for (const [k, v] of out.headers) {
|
||||||
if (responseHopByHop.has(k.toLowerCase())) {
|
if (RESPONSE_HOP_BY_HOP_HEADERS.has(k.toLowerCase())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
res.setHeader(k, v);
|
res.setHeader(k, v);
|
||||||
@ -208,7 +136,7 @@ const main = (): void => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = await readBodyBuffer(req);
|
const body = await readBodyBuffer(req, MAX_BODY_BYTES);
|
||||||
const targetUrl = `${upstream.baseUrl}${rest}${url.search}`;
|
const targetUrl = `${upstream.baseUrl}${rest}${url.search}`;
|
||||||
const headers = buildForwardHeaders(req, upstream.auth);
|
const headers = buildForwardHeaders(req, upstream.auth);
|
||||||
status = await proxyToUpstream(res, targetUrl, headers, body, method);
|
status = await proxyToUpstream(res, targetUrl, headers, body, method);
|
||||||
|
|||||||
17
services/smart-ide-sso-gateway/package-lock.json
generated
17
services/smart-ide-sso-gateway/package-lock.json
generated
@ -9,6 +9,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@4nk/smart-ide-http-utils": "file:../../packages/smart-ide-http-utils",
|
||||||
"@4nk/smart-ide-upstreams": "file:../../packages/smart-ide-upstreams",
|
"@4nk/smart-ide-upstreams": "file:../../packages/smart-ide-upstreams",
|
||||||
"jose": "^5.9.6"
|
"jose": "^5.9.6"
|
||||||
},
|
},
|
||||||
@ -20,6 +21,18 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"../../packages/smart-ide-http-utils": {
|
||||||
|
"name": "@4nk/smart-ide-http-utils",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.11.0",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"../../packages/smart-ide-upstreams": {
|
"../../packages/smart-ide-upstreams": {
|
||||||
"name": "@4nk/smart-ide-upstreams",
|
"name": "@4nk/smart-ide-upstreams",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
@ -32,6 +45,10 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@4nk/smart-ide-http-utils": {
|
||||||
|
"resolved": "../../packages/smart-ide-http-utils",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@4nk/smart-ide-upstreams": {
|
"node_modules/@4nk/smart-ide-upstreams": {
|
||||||
"resolved": "../../packages/smart-ide-upstreams",
|
"resolved": "../../packages/smart-ide-upstreams",
|
||||||
"link": true
|
"link": true
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@4nk/smart-ide-http-utils": "file:../../packages/smart-ide-http-utils",
|
||||||
"@4nk/smart-ide-upstreams": "file:../../packages/smart-ide-upstreams",
|
"@4nk/smart-ide-upstreams": "file:../../packages/smart-ide-upstreams",
|
||||||
"jose": "^5.9.6"
|
"jose": "^5.9.6"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -3,6 +3,13 @@ import type { JWTPayload } from "jose";
|
|||||||
import { appendSsoAccessLog } from "./accessLog.js";
|
import { appendSsoAccessLog } from "./accessLog.js";
|
||||||
import { discoverJwksUri, createVerify, type VerifyFn } from "./oidc.js";
|
import { discoverJwksUri, createVerify, type VerifyFn } from "./oidc.js";
|
||||||
import { listUpstreamKeys } from "@4nk/smart-ide-upstreams";
|
import { listUpstreamKeys } from "@4nk/smart-ide-upstreams";
|
||||||
|
import {
|
||||||
|
RESPONSE_HOP_BY_HOP_HEADERS,
|
||||||
|
copyHeadersForProxy,
|
||||||
|
isSafeProxyPath,
|
||||||
|
readBearer,
|
||||||
|
readBodyBuffer,
|
||||||
|
} from "@4nk/smart-ide-http-utils";
|
||||||
|
|
||||||
const HOST = process.env.SSO_GATEWAY_HOST ?? "127.0.0.1";
|
const HOST = process.env.SSO_GATEWAY_HOST ?? "127.0.0.1";
|
||||||
const PORT = Number(process.env.SSO_GATEWAY_PORT ?? "37148");
|
const PORT = Number(process.env.SSO_GATEWAY_PORT ?? "37148");
|
||||||
@ -71,83 +78,11 @@ const json = (res: http.ServerResponse, status: number, body: unknown): void =>
|
|||||||
res.end(JSON.stringify(body));
|
res.end(JSON.stringify(body));
|
||||||
};
|
};
|
||||||
|
|
||||||
const readBearer = (req: http.IncomingMessage): string | null => {
|
|
||||||
const raw = req.headers.authorization ?? "";
|
|
||||||
const m = /^Bearer\s+(.+)$/i.exec(raw);
|
|
||||||
return m?.[1]?.trim() ?? null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isSafeProxyPath = (p: string): boolean => {
|
|
||||||
if (!p.startsWith("/")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (const rawSeg of p.split("/")) {
|
|
||||||
if (rawSeg.length === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (rawSeg === "." || rawSeg === "..") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let seg: string;
|
|
||||||
try {
|
|
||||||
seg = decodeURIComponent(rawSeg);
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (seg === "." || seg === "..") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (seg.includes("/") || seg.includes("\\")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const readBodyBuffer = async (req: http.IncomingMessage): Promise<Buffer> => {
|
|
||||||
const chunks: Buffer[] = [];
|
|
||||||
let total = 0;
|
|
||||||
for await (const chunk of req) {
|
|
||||||
const b = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
||||||
total += b.length;
|
|
||||||
if (total > MAX_BODY_BYTES) {
|
|
||||||
throw new Error(`Request body exceeds ${MAX_BODY_BYTES} bytes`);
|
|
||||||
}
|
|
||||||
chunks.push(b);
|
|
||||||
}
|
|
||||||
return Buffer.concat(chunks);
|
|
||||||
};
|
|
||||||
|
|
||||||
const hopByHop = new Set([
|
|
||||||
"connection",
|
|
||||||
"keep-alive",
|
|
||||||
"proxy-authenticate",
|
|
||||||
"proxy-authorization",
|
|
||||||
"te",
|
|
||||||
"trailers",
|
|
||||||
"transfer-encoding",
|
|
||||||
"upgrade",
|
|
||||||
"host",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const buildForwardHeadersToGlobalApi = (
|
const buildForwardHeadersToGlobalApi = (
|
||||||
req: http.IncomingMessage,
|
req: http.IncomingMessage,
|
||||||
payload: JWTPayload,
|
payload: JWTPayload,
|
||||||
): Headers => {
|
): Headers => {
|
||||||
const out = new Headers();
|
const out = copyHeadersForProxy(req);
|
||||||
for (const [k, v] of Object.entries(req.headers)) {
|
|
||||||
if (!v) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const lk = k.toLowerCase();
|
|
||||||
if (hopByHop.has(lk)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (lk === "authorization") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
out.set(k, Array.isArray(v) ? v.join(", ") : v);
|
|
||||||
}
|
|
||||||
const gToken = globalApiToken();
|
const gToken = globalApiToken();
|
||||||
if (gToken) {
|
if (gToken) {
|
||||||
out.set("Authorization", `Bearer ${gToken}`);
|
out.set("Authorization", `Bearer ${gToken}`);
|
||||||
@ -163,13 +98,6 @@ const buildForwardHeadersToGlobalApi = (
|
|||||||
return out;
|
return out;
|
||||||
};
|
};
|
||||||
|
|
||||||
const responseHopByHop = new Set([
|
|
||||||
"connection",
|
|
||||||
"keep-alive",
|
|
||||||
"transfer-encoding",
|
|
||||||
"content-encoding",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const proxyToGlobalApi = async (
|
const proxyToGlobalApi = async (
|
||||||
req: http.IncomingMessage,
|
req: http.IncomingMessage,
|
||||||
res: http.ServerResponse,
|
res: http.ServerResponse,
|
||||||
@ -190,7 +118,7 @@ const proxyToGlobalApi = async (
|
|||||||
applyCors(res);
|
applyCors(res);
|
||||||
res.statusCode = out.status;
|
res.statusCode = out.status;
|
||||||
for (const [k, v] of out.headers) {
|
for (const [k, v] of out.headers) {
|
||||||
if (responseHopByHop.has(k.toLowerCase())) {
|
if (RESPONSE_HOP_BY_HOP_HEADERS.has(k.toLowerCase())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
res.setHeader(k, v);
|
res.setHeader(k, v);
|
||||||
@ -307,7 +235,7 @@ const main = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
logPath = pathname;
|
logPath = pathname;
|
||||||
const targetUrl = `${globalApiBase()}/v1/upstream/${upstreamKey}${rest}${url.search}`;
|
const targetUrl = `${globalApiBase()}/v1/upstream/${upstreamKey}${rest}${url.search}`;
|
||||||
const body = await readBodyBuffer(req);
|
const body = await readBodyBuffer(req, MAX_BODY_BYTES);
|
||||||
const headers = buildForwardHeadersToGlobalApi(req, payload);
|
const headers = buildForwardHeadersToGlobalApi(req, payload);
|
||||||
status = await proxyToGlobalApi(req, res, targetUrl, headers, body);
|
status = await proxyToGlobalApi(req, res, targetUrl, headers, body);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user