@duckfly/proxy 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/bin/cli.js +283 -0
- package/icons/logo-duck.png +0 -0
- package/package.json +50 -0
- package/src/api-client.js +83 -0
- package/src/proxy-server.js +425 -0
- package/src/request-queue.js +154 -0
- package/src/sanitizer.js +303 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Duckfly
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="icons/logo-duck.png" alt="Duckfly" width="140" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">Duckfly Proxy</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
Observe real API usage, continuously enrich documentation, and generate MCP servers with Duckfly.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/package/@duckfly/proxy"><img src="https://img.shields.io/npm/v/@duckfly/proxy.svg" alt="npm version" /></a>
|
|
13
|
+
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" /></a>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
## Overview
|
|
17
|
+
|
|
18
|
+
**Duckfly Proxy** is a lightweight HTTP proxy that observes real API usage and sends only **structural request metadata** to the Duckfly platform.
|
|
19
|
+
|
|
20
|
+
This allows your API documentation and MCP (Model Context Protocol) servers to be **continuously enriched** based on how your application actually behaves, keeping everything aligned as the system evolves.
|
|
21
|
+
|
|
22
|
+
## Why use Duckfly Proxy
|
|
23
|
+
|
|
24
|
+
- Continuously enriched API documentation
|
|
25
|
+
- MCP server generation for AI integrations
|
|
26
|
+
- Documentation aligned with real API usage
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
### Global install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install -g @duckfly/proxy
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Or run directly with npx
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx @duckfly/proxy
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
After running, the CLI will ask for:
|
|
43
|
+
|
|
44
|
+
| Prompt | Description | Default |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| **Token** | Your Duckfly application token | — |
|
|
47
|
+
| **Proxy Port** | Port where the proxy listens | `8080` |
|
|
48
|
+
| **Target URL** | Your backend API address | `http://localhost:3000` |
|
|
49
|
+
|
|
50
|
+
Configuration is saved locally in `.duckfly-proxy.json` so you don't need to re-enter it every time.
|
|
51
|
+
|
|
52
|
+
## How It Works
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
56
|
+
│ Application │ ───> │ Duckfly Proxy│ ───> │ Backend │
|
|
57
|
+
└─────────────┘ └──────────────┘ └──────────────┘
|
|
58
|
+
│
|
|
59
|
+
Sends API structure (with placeholders)
|
|
60
|
+
│
|
|
61
|
+
▼
|
|
62
|
+
┌─────────────────┐
|
|
63
|
+
│ Duckfly API │
|
|
64
|
+
│ Documentation & │
|
|
65
|
+
│ MCP Generation │
|
|
66
|
+
└─────────────────┘
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
1. Your application routes requests through Duckfly Proxy
|
|
70
|
+
2. The proxy forwards requests to your backend without changing behavior
|
|
71
|
+
3. Structural API information is sent to Duckfly
|
|
72
|
+
4. Documentation and MCP servers evolve continuously
|
|
73
|
+
|
|
74
|
+
## Data Handling
|
|
75
|
+
|
|
76
|
+
Duckfly Proxy processes only technical and structural information such as HTTP methods, routes, headers, and payload formats.
|
|
77
|
+
|
|
78
|
+
Request and response values are replaced with **placeholders** before being sent, ensuring no sensitive or user-specific data is transmitted.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT License
|
|
83
|
+
|
|
84
|
+
This license applies only to the Duckfly Proxy package. The Duckfly platform and services are governed by separate terms.
|
|
85
|
+
|
|
86
|
+
<p align="center">Made with 🦆 by Duckfly</p>
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const inquirer = require('inquirer');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const { Spinner } = require('cli-spinner');
|
|
6
|
+
const ProxyServer = require('../src/proxy-server');
|
|
7
|
+
const ApiClient = require('../src/api-client');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const yellow = chalk.hex('#FFD54F');
|
|
12
|
+
const gold = chalk.hex('#FFC107');
|
|
13
|
+
|
|
14
|
+
const DUCKFLY_LOGO = `
|
|
15
|
+
${yellow(`
|
|
16
|
+
@@@@@@@@@
|
|
17
|
+
@%+-------=%@
|
|
18
|
+
@@+-----------=@@
|
|
19
|
+
@@=-----=##=-----@@
|
|
20
|
+
@*------@@=#---+*%@
|
|
21
|
+
@+------+%%+-=%*+*%@@
|
|
22
|
+
@*---------=#%++%+++#%%@
|
|
23
|
+
@@=-------=@%#++++++++%@
|
|
24
|
+
@@+-------=*%%@@@@@@@
|
|
25
|
+
@@@*-------===%@
|
|
26
|
+
@%+---------------#@
|
|
27
|
+
@%----------+--------#@
|
|
28
|
+
@@@@%=------------#-------=@@
|
|
29
|
+
@*------+*=-------%-------=@@
|
|
30
|
+
@%--+@*+---------##-------+@
|
|
31
|
+
@%=-=#%%#+----+%*------=+@@
|
|
32
|
+
@@*==+%%##%%#=-----===*@@@
|
|
33
|
+
@@#=============+*%#+=+*@@
|
|
34
|
+
@@@%****#%%*+#@@@@%@@@
|
|
35
|
+
@@@@%#++#@@@
|
|
36
|
+
`)}
|
|
37
|
+
${gold('═══════════════════════════════════════════════════════════════════════════════════════════')}
|
|
38
|
+
${chalk.bold.yellow(' 🦆 DUCKFLY PROXY v1.0.0 ')}
|
|
39
|
+
${gold('═══════════════════════════════════════════════════════════════════════════════════════════')}
|
|
40
|
+
${chalk.gray(' Capture and document your APIs automatically ')}
|
|
41
|
+
${chalk.gray(' https://duckfly.dev ')}
|
|
42
|
+
${gold('═══════════════════════════════════════════════════════════════════════════════════════════')}
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
const DUCKFLY_API_URL = 'https://api.duckfly.dev';
|
|
46
|
+
const DUCKFLY_PROXY_URL = 'https://proxy.duckfly.dev';
|
|
47
|
+
|
|
48
|
+
let config = {
|
|
49
|
+
token: null,
|
|
50
|
+
appName: null,
|
|
51
|
+
appUrl: null,
|
|
52
|
+
domainPackageId: null,
|
|
53
|
+
proxyPort: 8080,
|
|
54
|
+
targetUrl: 'http://localhost:3000',
|
|
55
|
+
apiUrl: DUCKFLY_API_URL,
|
|
56
|
+
proxyUrl: DUCKFLY_PROXY_URL
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const CONFIG_FILE = path.join(process.cwd(), '.duckfly-proxy.json');
|
|
60
|
+
|
|
61
|
+
function loadConfig() {
|
|
62
|
+
try {
|
|
63
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
64
|
+
const saved = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
65
|
+
config = { ...config, ...saved };
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.log(chalk.yellow('⚠️ Erro ao carregar configuração salva'));
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function saveConfig() {
|
|
75
|
+
try {
|
|
76
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.log(chalk.yellow('⚠️ Não foi possível salvar a configuração'));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function validateToken(token) {
|
|
83
|
+
const spinner = new Spinner(chalk.yellow('%s Validando token...'));
|
|
84
|
+
spinner.setSpinnerString('|/-\\');
|
|
85
|
+
spinner.start();
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const apiClient = new ApiClient(config.apiUrl, config.proxyUrl, token);
|
|
89
|
+
const result = await apiClient.validateToken();
|
|
90
|
+
|
|
91
|
+
spinner.stop(true);
|
|
92
|
+
|
|
93
|
+
if (result.valid) {
|
|
94
|
+
console.log(chalk.green('✅ Token validado com sucesso!\n'));
|
|
95
|
+
console.log(chalk.bold.white('📋 Informações da Aplicação:'));
|
|
96
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
97
|
+
console.log(chalk.yellow(' Nome: ') + chalk.white(result.appName));
|
|
98
|
+
console.log(chalk.yellow(' URL: ') + chalk.white(result.appUrl));
|
|
99
|
+
console.log(chalk.yellow(' Package ID: ') + chalk.white(result.domainPackageId));
|
|
100
|
+
console.log(chalk.gray('─'.repeat(60)) + '\n');
|
|
101
|
+
|
|
102
|
+
config.appName = result.appName;
|
|
103
|
+
config.appUrl = result.appUrl;
|
|
104
|
+
config.domainPackageId = result.domainPackageId;
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log(chalk.red('❌ Token inválido ou expirado\n'));
|
|
109
|
+
return false;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
spinner.stop(true);
|
|
112
|
+
console.log(chalk.red('❌ Erro ao validar token: ') + chalk.gray(error.message) + '\n');
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function askQuestions() {
|
|
118
|
+
if (!config.token) {
|
|
119
|
+
const { token } = await inquirer.prompt([
|
|
120
|
+
{
|
|
121
|
+
type: 'password',
|
|
122
|
+
name: 'token',
|
|
123
|
+
message: chalk.yellow('🔑 Cole seu token do Duckfly:'),
|
|
124
|
+
mask: '*',
|
|
125
|
+
validate: (input) => {
|
|
126
|
+
if (!input || input.trim().length === 0) {
|
|
127
|
+
return 'Token não pode ser vazio';
|
|
128
|
+
}
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
config.token = token;
|
|
135
|
+
|
|
136
|
+
console.log('');
|
|
137
|
+
const valid = await validateToken(config.token);
|
|
138
|
+
|
|
139
|
+
if (!valid) {
|
|
140
|
+
console.log(chalk.red('❌ Token inválido. Não é possível continuar.\n'));
|
|
141
|
+
console.log(chalk.yellow('💡 Obtenha um token válido em: ') + chalk.white('https://duckfly.dev\n'));
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
config.apiUrl = DUCKFLY_API_URL;
|
|
147
|
+
config.proxyUrl = DUCKFLY_PROXY_URL;
|
|
148
|
+
|
|
149
|
+
const questions = [
|
|
150
|
+
{
|
|
151
|
+
type: 'input',
|
|
152
|
+
name: 'proxyPort',
|
|
153
|
+
message: chalk.yellow('🔌 Porta do proxy (porta que vai receber as requisições):'),
|
|
154
|
+
default: config.proxyPort,
|
|
155
|
+
validate: (input) => {
|
|
156
|
+
const port = parseInt(input);
|
|
157
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
158
|
+
return 'Porta inválida (1-65535)';
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
type: 'input',
|
|
165
|
+
name: 'targetUrl',
|
|
166
|
+
message: chalk.yellow('🎯 URL de destino (para onde encaminhar as requisições):'),
|
|
167
|
+
default: config.targetUrl,
|
|
168
|
+
validate: (input) => {
|
|
169
|
+
try {
|
|
170
|
+
new URL(input);
|
|
171
|
+
return true;
|
|
172
|
+
} catch {
|
|
173
|
+
return 'URL inválida (ex: http://localhost:3000)';
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
const answers = await inquirer.prompt(questions);
|
|
180
|
+
|
|
181
|
+
Object.assign(config, answers);
|
|
182
|
+
|
|
183
|
+
return answers;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let proxyServer = null;
|
|
187
|
+
|
|
188
|
+
async function startProxy() {
|
|
189
|
+
console.log(chalk.yellow('\n🚀 Iniciando Duckfly Proxy...\n'));
|
|
190
|
+
|
|
191
|
+
proxyServer = new ProxyServer(config);
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
await proxyServer.start();
|
|
195
|
+
|
|
196
|
+
console.log(chalk.green('✅ Proxy iniciado com sucesso!\n'));
|
|
197
|
+
console.log(chalk.bold.white('📊 Status:'));
|
|
198
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
199
|
+
console.log(chalk.yellow(' Aplicação: ') + chalk.white(config.appName));
|
|
200
|
+
console.log(chalk.yellow(' Proxy rodando em: ') + chalk.white(`http://localhost:${config.proxyPort}`));
|
|
201
|
+
console.log(chalk.yellow(' Encaminhando para: ') + chalk.white(config.targetUrl));
|
|
202
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
203
|
+
console.log(chalk.yellow('\n💡 Dica: Configure sua aplicação para apontar para ') + chalk.white(`http://localhost:${config.proxyPort}`));
|
|
204
|
+
console.log(chalk.yellow(' Todas as requisições serão capturadas e documentadas automaticamente!\n'));
|
|
205
|
+
console.log(chalk.gray('Pressione Ctrl+C para parar o proxy\n'));
|
|
206
|
+
|
|
207
|
+
saveConfig();
|
|
208
|
+
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.log(chalk.red('❌ Erro ao iniciar proxy: ') + chalk.gray(error.message));
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function main() {
|
|
216
|
+
console.clear();
|
|
217
|
+
|
|
218
|
+
console.log(DUCKFLY_LOGO);
|
|
219
|
+
|
|
220
|
+
const hasConfig = loadConfig();
|
|
221
|
+
let useExisting = false;
|
|
222
|
+
|
|
223
|
+
if (hasConfig) {
|
|
224
|
+
console.log(chalk.green('✅ Configuração anterior encontrada!\n'));
|
|
225
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
226
|
+
console.log(chalk.yellow(' App: ') + chalk.white(config.appName || 'N/A'));
|
|
227
|
+
console.log(chalk.yellow(' Porta: ') + chalk.white(config.proxyPort));
|
|
228
|
+
console.log(chalk.yellow(' Alvo: ') + chalk.white(config.targetUrl));
|
|
229
|
+
console.log(chalk.gray('─'.repeat(60)) + '\n');
|
|
230
|
+
|
|
231
|
+
const answer = await inquirer.prompt([
|
|
232
|
+
{
|
|
233
|
+
type: 'confirm',
|
|
234
|
+
name: 'useExisting',
|
|
235
|
+
message: chalk.yellow('Deseja usar a configuração salva?'),
|
|
236
|
+
default: true
|
|
237
|
+
}
|
|
238
|
+
]);
|
|
239
|
+
|
|
240
|
+
useExisting = answer.useExisting;
|
|
241
|
+
|
|
242
|
+
if (!useExisting) {
|
|
243
|
+
config.token = null;
|
|
244
|
+
config.appName = null;
|
|
245
|
+
config.proxyPort = 8080;
|
|
246
|
+
config.targetUrl = 'http://localhost:3000';
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!useExisting) {
|
|
251
|
+
await askQuestions();
|
|
252
|
+
} else {
|
|
253
|
+
console.log('');
|
|
254
|
+
const valid = await validateToken(config.token);
|
|
255
|
+
|
|
256
|
+
if (!valid) {
|
|
257
|
+
console.log(chalk.red('❌ Token salvo está inválido. Configurando novamente...\n'));
|
|
258
|
+
config.token = null;
|
|
259
|
+
await askQuestions();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
await startProxy();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
process.on('unhandledRejection', (error) => {
|
|
267
|
+
console.error(chalk.red('\n❌ Erro não tratado:'), error);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
process.on('SIGINT', async () => {
|
|
272
|
+
console.log(chalk.yellow('\n\n👋 Encerrando Duckfly Proxy...'));
|
|
273
|
+
if (proxyServer) {
|
|
274
|
+
await proxyServer.stop();
|
|
275
|
+
}
|
|
276
|
+
console.log(chalk.gray('Até logo! 🦆\n'));
|
|
277
|
+
process.exit(0);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
main().catch(error => {
|
|
281
|
+
console.error(chalk.red('❌ Erro fatal:'), error);
|
|
282
|
+
process.exit(1);
|
|
283
|
+
});
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@duckfly/proxy",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Duckfly Proxy observes real API usage to continuously enrich and generate API documentation and MCP servers on Duckfly.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"duckfly-proxy": "./bin/cli.js",
|
|
8
|
+
"duckfly": "./bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node bin/cli.js",
|
|
12
|
+
"dev": "node bin/cli.js",
|
|
13
|
+
"build": "echo 'No build needed'",
|
|
14
|
+
"test": "echo 'No tests yet'"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"proxy",
|
|
18
|
+
"api",
|
|
19
|
+
"documentation",
|
|
20
|
+
"duckfly",
|
|
21
|
+
"http",
|
|
22
|
+
"request",
|
|
23
|
+
"capture"
|
|
24
|
+
],
|
|
25
|
+
"author": "Duckfly",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"inquirer": "^8.2.6",
|
|
29
|
+
"chalk": "^4.1.2",
|
|
30
|
+
"http-proxy": "^1.18.1",
|
|
31
|
+
"axios": "^1.6.5",
|
|
32
|
+
"express": "^4.18.2",
|
|
33
|
+
"cli-spinner": "^0.2.10"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=14.0.0"
|
|
38
|
+
},
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/duckfly-dev/proxy"
|
|
42
|
+
},
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/duckfly-dev/proxy/issues"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://duckfly.dev",
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
|
|
3
|
+
class ApiClient {
|
|
4
|
+
constructor(apiUrl, proxyUrl, token) {
|
|
5
|
+
this.apiUrl = apiUrl;
|
|
6
|
+
this.proxyUrl = proxyUrl;
|
|
7
|
+
this.token = token;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async validateToken() {
|
|
11
|
+
try {
|
|
12
|
+
const url = `${this.apiUrl}/internal/v1/core/token/${this.token}`;
|
|
13
|
+
|
|
14
|
+
const response = await axios.get(url, {
|
|
15
|
+
timeout: 30000,
|
|
16
|
+
headers: {
|
|
17
|
+
'User-Agent': 'Duckfly-Proxy/1.0.0'
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
valid: true,
|
|
23
|
+
appName: response.data.name || 'Aplicação',
|
|
24
|
+
appUrl: response.data.url || '',
|
|
25
|
+
domainPackageId: response.data.domainPackageId || ''
|
|
26
|
+
};
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error.response) {
|
|
29
|
+
return {
|
|
30
|
+
valid: false,
|
|
31
|
+
error: `HTTP ${error.response.status}: ${error.response.statusText}`
|
|
32
|
+
};
|
|
33
|
+
} else if (error.request) {
|
|
34
|
+
return {
|
|
35
|
+
valid: false,
|
|
36
|
+
error: 'Servidor não respondeu. Verifique sua conexão.'
|
|
37
|
+
};
|
|
38
|
+
} else {
|
|
39
|
+
return {
|
|
40
|
+
valid: false,
|
|
41
|
+
error: error.message
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async sendRequest(capturedData) {
|
|
48
|
+
try {
|
|
49
|
+
const url = `${this.proxyUrl}/api/proxy`;
|
|
50
|
+
|
|
51
|
+
const response = await axios.post(url, capturedData, {
|
|
52
|
+
timeout: 30000,
|
|
53
|
+
headers: {
|
|
54
|
+
'Authorization': `Bearer ${this.token}`,
|
|
55
|
+
'Content-Type': 'application/json',
|
|
56
|
+
'User-Agent': 'Duckfly-Proxy/1.0.0'
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
success: true,
|
|
62
|
+
data: response.data
|
|
63
|
+
};
|
|
64
|
+
} catch (error) {
|
|
65
|
+
let errorMessage = 'Unknown error';
|
|
66
|
+
|
|
67
|
+
if (error.response) {
|
|
68
|
+
errorMessage = `HTTP ${error.response.status}: ${error.response.statusText}`;
|
|
69
|
+
} else if (error.request) {
|
|
70
|
+
errorMessage = 'No response from server';
|
|
71
|
+
} else {
|
|
72
|
+
errorMessage = error.message;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
success: false,
|
|
77
|
+
error: errorMessage
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = ApiClient;
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
const httpProxy = require('http-proxy');
|
|
2
|
+
const express = require('express');
|
|
3
|
+
const { Readable } = require('stream');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const ApiClient = require('./api-client');
|
|
6
|
+
const RequestQueue = require('./request-queue');
|
|
7
|
+
const { sanitizeRequestData } = require('./sanitizer');
|
|
8
|
+
|
|
9
|
+
class ProxyServer {
|
|
10
|
+
constructor(config) {
|
|
11
|
+
this.config = config;
|
|
12
|
+
this.proxy = httpProxy.createProxyServer({});
|
|
13
|
+
this.app = express();
|
|
14
|
+
this.server = null;
|
|
15
|
+
this.apiClient = new ApiClient(config.apiUrl, config.proxyUrl, config.token);
|
|
16
|
+
this.requestQueue = new RequestQueue(this.apiClient);
|
|
17
|
+
|
|
18
|
+
this.stats = {
|
|
19
|
+
totalRequests: 0,
|
|
20
|
+
captured: 0,
|
|
21
|
+
sent: 0,
|
|
22
|
+
failed: 0,
|
|
23
|
+
startTime: null
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
this.statsShown = false;
|
|
27
|
+
this.statsInterval = null;
|
|
28
|
+
|
|
29
|
+
this.setupProxyHandlers();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
setupProxyHandlers() {
|
|
33
|
+
this.proxy.on('proxyReq', (proxyReq, req, res) => {
|
|
34
|
+
this.stats.totalRequests++;
|
|
35
|
+
|
|
36
|
+
console.log(
|
|
37
|
+
chalk.blue('→'),
|
|
38
|
+
chalk.white(req.method),
|
|
39
|
+
chalk.gray(req.url),
|
|
40
|
+
chalk.dim(`[${this.stats.totalRequests}]`)
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
this.captureRequest(req, res);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
this.proxy.on('proxyRes', (proxyRes, req, res) => {
|
|
47
|
+
let bodyChunks = [];
|
|
48
|
+
|
|
49
|
+
proxyRes.on('data', (chunk) => {
|
|
50
|
+
bodyChunks.push(chunk);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
proxyRes.on('end', () => {
|
|
54
|
+
const responseBody = Buffer.concat(bodyChunks);
|
|
55
|
+
|
|
56
|
+
const statusColor = proxyRes.statusCode >= 400 ? chalk.red :
|
|
57
|
+
proxyRes.statusCode >= 300 ? chalk.yellow :
|
|
58
|
+
chalk.green;
|
|
59
|
+
|
|
60
|
+
console.log(
|
|
61
|
+
chalk.blue('←'),
|
|
62
|
+
statusColor(proxyRes.statusCode),
|
|
63
|
+
chalk.gray(req.url),
|
|
64
|
+
chalk.dim(`${responseBody.length} bytes`)
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
this.captureResponse(req, proxyRes, responseBody);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
this.proxy.on('error', (err, req, res) => {
|
|
72
|
+
console.log(chalk.red('✗'), chalk.white('Erro no proxy:'), chalk.gray(err.message));
|
|
73
|
+
|
|
74
|
+
if (!res.headersSent && !res.destroyed) {
|
|
75
|
+
res.writeHead(500, {
|
|
76
|
+
'Content-Type': 'application/json'
|
|
77
|
+
});
|
|
78
|
+
res.end(JSON.stringify({
|
|
79
|
+
error: 'Proxy Error',
|
|
80
|
+
message: err.message
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
captureRequest(req, res) {
|
|
87
|
+
try {
|
|
88
|
+
req._captureData = {
|
|
89
|
+
method: req.method,
|
|
90
|
+
url: req.url,
|
|
91
|
+
headers: { ...req.headers },
|
|
92
|
+
body: this.parseBody(req.rawBody, req.headers['content-type']),
|
|
93
|
+
protocol: req.socket.encrypted ? 'https' : 'http',
|
|
94
|
+
timestamp: new Date().toISOString(),
|
|
95
|
+
socket: {
|
|
96
|
+
localPort: req.socket.localPort,
|
|
97
|
+
remoteAddress: req.socket.remoteAddress
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.log(chalk.yellow('⚠'), chalk.gray('Erro ao capturar requisição:'), error.message);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
captureResponse(req, proxyRes, responseBody) {
|
|
106
|
+
try {
|
|
107
|
+
if (!req._captureData) return;
|
|
108
|
+
|
|
109
|
+
const urlObj = new URL(req._captureData.url, `${req._captureData.protocol}://localhost`);
|
|
110
|
+
const queryParams = {};
|
|
111
|
+
urlObj.searchParams.forEach((value, key) => {
|
|
112
|
+
queryParams[key] = value;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const securityInfo = this.detectSecurity(req._captureData.headers, queryParams);
|
|
116
|
+
|
|
117
|
+
const responseData = {
|
|
118
|
+
statusCode: proxyRes.statusCode,
|
|
119
|
+
statusMessage: proxyRes.statusMessage,
|
|
120
|
+
headers: { ...proxyRes.headers },
|
|
121
|
+
body: this.parseBody(responseBody, proxyRes.headers['content-type'])
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const capturedData = {
|
|
125
|
+
req: {
|
|
126
|
+
method: req._captureData.method,
|
|
127
|
+
protocol: req._captureData.protocol,
|
|
128
|
+
headers: req._captureData.headers,
|
|
129
|
+
originalUrl: req._captureData.url,
|
|
130
|
+
body: req._captureData.body,
|
|
131
|
+
socket: req._captureData.socket,
|
|
132
|
+
security: securityInfo.length > 0 ? securityInfo : undefined
|
|
133
|
+
},
|
|
134
|
+
res: responseData
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const sanitizedCapturedData = sanitizeRequestData(capturedData);
|
|
138
|
+
|
|
139
|
+
this.requestQueue.add(sanitizedCapturedData);
|
|
140
|
+
this.stats.captured++;
|
|
141
|
+
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.log(chalk.yellow('⚠'), chalk.gray('Erro ao capturar resposta:'), error.message);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
detectSecurity(headers, queryParams = {}) {
|
|
148
|
+
const security = [];
|
|
149
|
+
const normalizedHeaders = {};
|
|
150
|
+
|
|
151
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
152
|
+
normalizedHeaders[key.toLowerCase()] = value;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (normalizedHeaders['authorization']) {
|
|
156
|
+
const authValue = normalizedHeaders['authorization'];
|
|
157
|
+
|
|
158
|
+
if (authValue.toLowerCase().startsWith('bearer ')) {
|
|
159
|
+
const token = authValue.substring(7).trim();
|
|
160
|
+
const bearerFormat = this.detectBearerFormat(token);
|
|
161
|
+
|
|
162
|
+
security.push({
|
|
163
|
+
type: 'http',
|
|
164
|
+
scheme: 'bearer',
|
|
165
|
+
in: 'header',
|
|
166
|
+
name: 'authorization',
|
|
167
|
+
bearerFormat: bearerFormat
|
|
168
|
+
});
|
|
169
|
+
} else if (authValue.toLowerCase().startsWith('basic ')) {
|
|
170
|
+
security.push({
|
|
171
|
+
type: 'http',
|
|
172
|
+
scheme: 'basic',
|
|
173
|
+
in: 'header',
|
|
174
|
+
name: 'authorization'
|
|
175
|
+
});
|
|
176
|
+
} else if (authValue.toLowerCase().startsWith('digest ')) {
|
|
177
|
+
security.push({
|
|
178
|
+
type: 'http',
|
|
179
|
+
scheme: 'digest',
|
|
180
|
+
in: 'header',
|
|
181
|
+
name: 'authorization'
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const apiKeyHeaders = [
|
|
187
|
+
'x-api-key',
|
|
188
|
+
'api-key',
|
|
189
|
+
'apikey',
|
|
190
|
+
'x-api-token',
|
|
191
|
+
'api-token',
|
|
192
|
+
'apitoken',
|
|
193
|
+
'x-auth-token',
|
|
194
|
+
'access-token',
|
|
195
|
+
'x-access-token'
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
for (const headerName of apiKeyHeaders) {
|
|
199
|
+
if (normalizedHeaders[headerName]) {
|
|
200
|
+
security.push({
|
|
201
|
+
type: 'apiKey',
|
|
202
|
+
in: 'header',
|
|
203
|
+
name: headerName
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const apiKeyParams = [
|
|
209
|
+
'api_key',
|
|
210
|
+
'apikey',
|
|
211
|
+
'api-key',
|
|
212
|
+
'key',
|
|
213
|
+
'token',
|
|
214
|
+
'access_token',
|
|
215
|
+
'auth_token'
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
for (const paramName of apiKeyParams) {
|
|
219
|
+
if (queryParams[paramName]) {
|
|
220
|
+
security.push({
|
|
221
|
+
type: 'apiKey',
|
|
222
|
+
in: 'query',
|
|
223
|
+
name: paramName
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (normalizedHeaders['cookie']) {
|
|
229
|
+
const cookies = this.parseCookieHeader(normalizedHeaders['cookie']);
|
|
230
|
+
const authCookies = [
|
|
231
|
+
'session',
|
|
232
|
+
'sessionid',
|
|
233
|
+
'session_id',
|
|
234
|
+
'sid',
|
|
235
|
+
'auth',
|
|
236
|
+
'auth_token',
|
|
237
|
+
'token',
|
|
238
|
+
'access_token'
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
for (const cookieName of authCookies) {
|
|
242
|
+
if (cookies[cookieName]) {
|
|
243
|
+
security.push({
|
|
244
|
+
type: 'apiKey',
|
|
245
|
+
in: 'cookie',
|
|
246
|
+
name: cookieName
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return security;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
detectBearerFormat(token) {
|
|
256
|
+
if (token.split('.').length === 3) {
|
|
257
|
+
return 'JWT';
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(token)) {
|
|
261
|
+
return 'UUID';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
parseCookieHeader(cookieHeader) {
|
|
268
|
+
const cookies = {};
|
|
269
|
+
if (!cookieHeader) return cookies;
|
|
270
|
+
|
|
271
|
+
const parts = cookieHeader.split(';');
|
|
272
|
+
for (const part of parts) {
|
|
273
|
+
const [name, ...valueParts] = part.split('=');
|
|
274
|
+
if (name && valueParts.length > 0) {
|
|
275
|
+
cookies[name.trim().toLowerCase()] = valueParts.join('=').trim();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return cookies;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
parseBody(buffer, contentType = '') {
|
|
283
|
+
if (!buffer || buffer.length === 0) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const MAX_SIZE = 10 * 1024 * 1024;
|
|
288
|
+
if (buffer.length > MAX_SIZE) {
|
|
289
|
+
return `[Body too large: ${buffer.length} bytes]`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const binaryTypes = ['image/', 'video/', 'audio/', 'application/octet-stream', 'application/pdf'];
|
|
293
|
+
if (binaryTypes.some(type => contentType.toLowerCase().includes(type))) {
|
|
294
|
+
return `[Binary content: ${contentType}, ${buffer.length} bytes]`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
const text = buffer.toString('utf8');
|
|
299
|
+
|
|
300
|
+
if (contentType.includes('application/json')) {
|
|
301
|
+
try {
|
|
302
|
+
return JSON.parse(text);
|
|
303
|
+
} catch {
|
|
304
|
+
return text;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
309
|
+
const params = new URLSearchParams(text);
|
|
310
|
+
const obj = {};
|
|
311
|
+
for (const [key, value] of params) {
|
|
312
|
+
obj[key] = value;
|
|
313
|
+
}
|
|
314
|
+
return obj;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (contentType.includes('multipart/form-data')) {
|
|
318
|
+
return '[Multipart form data]';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return text;
|
|
322
|
+
} catch (error) {
|
|
323
|
+
return '[Error parsing body]';
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async start() {
|
|
328
|
+
return new Promise((resolve, reject) => {
|
|
329
|
+
this.app.use((req, res) => {
|
|
330
|
+
const chunks = [];
|
|
331
|
+
|
|
332
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
333
|
+
|
|
334
|
+
req.on('end', () => {
|
|
335
|
+
req.rawBody = Buffer.concat(chunks);
|
|
336
|
+
|
|
337
|
+
const bufferStream = new Readable();
|
|
338
|
+
bufferStream.push(req.rawBody);
|
|
339
|
+
bufferStream.push(null);
|
|
340
|
+
|
|
341
|
+
this.proxy.web(req, res, {
|
|
342
|
+
target: this.config.targetUrl,
|
|
343
|
+
changeOrigin: true,
|
|
344
|
+
preserveHeaderKeyCase: true,
|
|
345
|
+
buffer: bufferStream
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
req.on('error', (err) => {
|
|
350
|
+
console.log(chalk.red('✗'), chalk.white('Erro ao ler requisição:'), chalk.gray(err.message));
|
|
351
|
+
if (!res.headersSent && !res.destroyed) {
|
|
352
|
+
res.writeHead(502, { 'Content-Type': 'application/json' });
|
|
353
|
+
res.end(JSON.stringify({ error: 'Request Error', message: err.message }));
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
this.server = this.app.listen(this.config.proxyPort, (err) => {
|
|
359
|
+
if (err) {
|
|
360
|
+
reject(err);
|
|
361
|
+
} else {
|
|
362
|
+
this.stats.startTime = new Date();
|
|
363
|
+
|
|
364
|
+
this.requestQueue.on('sent', () => {
|
|
365
|
+
this.stats.sent++;
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
this.requestQueue.on('failed', () => {
|
|
369
|
+
this.stats.failed++;
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
this.requestQueue.start();
|
|
373
|
+
|
|
374
|
+
this.statsInterval = setInterval(() => {
|
|
375
|
+
this.logStats();
|
|
376
|
+
}, 30000);
|
|
377
|
+
|
|
378
|
+
resolve();
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
logStats() {
|
|
385
|
+
const uptime = Math.floor((Date.now() - this.stats.startTime) / 1000);
|
|
386
|
+
const hours = Math.floor(uptime / 3600);
|
|
387
|
+
const minutes = Math.floor((uptime % 3600) / 60);
|
|
388
|
+
const seconds = uptime % 60;
|
|
389
|
+
|
|
390
|
+
if (this.statsShown) {
|
|
391
|
+
process.stdout.write('\x1b[9A');
|
|
392
|
+
}
|
|
393
|
+
this.statsShown = true;
|
|
394
|
+
|
|
395
|
+
process.stdout.write('\r' + chalk.gray('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') + '\x1b[K\n');
|
|
396
|
+
process.stdout.write('\r' + chalk.bold.white('📊 Estatísticas:') + '\x1b[K\n');
|
|
397
|
+
process.stdout.write('\r' + chalk.yellow(' Uptime: ') + chalk.white(`${hours}h ${minutes}m ${seconds}s`) + '\x1b[K\n');
|
|
398
|
+
process.stdout.write('\r' + chalk.yellow(' Total: ') + chalk.white(this.stats.totalRequests) + '\x1b[K\n');
|
|
399
|
+
process.stdout.write('\r' + chalk.yellow(' Capturadas: ') + chalk.white(this.stats.captured) + '\x1b[K\n');
|
|
400
|
+
process.stdout.write('\r' + chalk.yellow(' Enviadas: ') + chalk.green(this.stats.sent) + '\x1b[K\n');
|
|
401
|
+
process.stdout.write('\r' + chalk.yellow(' Falhas: ') + (this.stats.failed > 0 ? chalk.red(this.stats.failed) : chalk.gray(this.stats.failed)) + '\x1b[K\n');
|
|
402
|
+
process.stdout.write('\r' + chalk.yellow(' Na fila: ') + chalk.yellow(this.requestQueue.getQueueSize()) + '\x1b[K\n');
|
|
403
|
+
process.stdout.write('\r' + chalk.gray('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') + '\x1b[K\n');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async stop() {
|
|
407
|
+
return new Promise((resolve) => {
|
|
408
|
+
if (this.statsInterval) {
|
|
409
|
+
clearInterval(this.statsInterval);
|
|
410
|
+
this.statsInterval = null;
|
|
411
|
+
}
|
|
412
|
+
if (this.server) {
|
|
413
|
+
this.requestQueue.stop();
|
|
414
|
+
this.server.close(() => {
|
|
415
|
+
console.log(chalk.yellow('Proxy encerrado'));
|
|
416
|
+
resolve();
|
|
417
|
+
});
|
|
418
|
+
} else {
|
|
419
|
+
resolve();
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
module.exports = ProxyServer;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
const EventEmitter = require('events');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
|
|
4
|
+
class RequestQueue extends EventEmitter {
|
|
5
|
+
constructor(apiClient, options = {}) {
|
|
6
|
+
super();
|
|
7
|
+
|
|
8
|
+
this.apiClient = apiClient;
|
|
9
|
+
this.queue = [];
|
|
10
|
+
this.isProcessing = false;
|
|
11
|
+
this.isRunning = false;
|
|
12
|
+
|
|
13
|
+
this.options = {
|
|
14
|
+
maxQueueSize: options.maxQueueSize || 1000,
|
|
15
|
+
retryAttempts: options.retryAttempts || 3,
|
|
16
|
+
retryDelay: options.retryDelay || 1000,
|
|
17
|
+
processDelay: options.processDelay || 100,
|
|
18
|
+
batchSize: options.batchSize || 1
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
this.stats = {
|
|
22
|
+
added: 0,
|
|
23
|
+
processed: 0,
|
|
24
|
+
sent: 0,
|
|
25
|
+
failed: 0,
|
|
26
|
+
dropped: 0
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
add(requestData) {
|
|
31
|
+
if (this.queue.length >= this.options.maxQueueSize) {
|
|
32
|
+
this.queue.shift();
|
|
33
|
+
this.stats.dropped++;
|
|
34
|
+
console.log(chalk.yellow('⚠'), chalk.gray('Fila cheia, item mais antigo descartado'));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.queue.push({
|
|
38
|
+
data: requestData,
|
|
39
|
+
retryCount: 0,
|
|
40
|
+
addedAt: Date.now()
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
this.stats.added++;
|
|
44
|
+
|
|
45
|
+
if (this.isRunning && !this.isProcessing) {
|
|
46
|
+
this.processQueue();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async processQueue() {
|
|
51
|
+
if (this.isProcessing || this.queue.length === 0) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.isProcessing = true;
|
|
56
|
+
|
|
57
|
+
while (this.isRunning && this.queue.length > 0) {
|
|
58
|
+
const item = this.queue.shift();
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
await this.sendRequest(item);
|
|
62
|
+
} catch (error) { }
|
|
63
|
+
|
|
64
|
+
if (this.queue.length > 0) {
|
|
65
|
+
await this.delay(this.options.processDelay);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.isProcessing = false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async sendRequest(item) {
|
|
73
|
+
try {
|
|
74
|
+
const result = await this.apiClient.sendRequest(item.data);
|
|
75
|
+
|
|
76
|
+
if (result.success) {
|
|
77
|
+
this.stats.processed++;
|
|
78
|
+
this.stats.sent++;
|
|
79
|
+
this.emit('sent', item.data);
|
|
80
|
+
|
|
81
|
+
console.log(
|
|
82
|
+
chalk.green('✓'),
|
|
83
|
+
chalk.gray('Enviado para API'),
|
|
84
|
+
chalk.dim(`[${item.data.req.method} ${item.data.req.originalUrl}]`)
|
|
85
|
+
);
|
|
86
|
+
} else {
|
|
87
|
+
throw new Error(result.error);
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
item.retryCount++;
|
|
91
|
+
|
|
92
|
+
if (item.retryCount < this.options.retryAttempts) {
|
|
93
|
+
console.log(
|
|
94
|
+
chalk.yellow('⟳'),
|
|
95
|
+
chalk.gray(`Retry ${item.retryCount}/${this.options.retryAttempts}:`),
|
|
96
|
+
chalk.dim(error.message)
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
await this.delay(this.options.retryDelay * item.retryCount);
|
|
100
|
+
|
|
101
|
+
this.queue.unshift(item);
|
|
102
|
+
} else {
|
|
103
|
+
this.stats.processed++;
|
|
104
|
+
this.stats.failed++;
|
|
105
|
+
this.emit('failed', item.data, error);
|
|
106
|
+
|
|
107
|
+
console.log(
|
|
108
|
+
chalk.red('✗'),
|
|
109
|
+
chalk.gray('Falha ao enviar após retries:'),
|
|
110
|
+
chalk.dim(error.message)
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
delay(ms) {
|
|
117
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
start() {
|
|
121
|
+
this.isRunning = true;
|
|
122
|
+
console.log(chalk.cyan('▶'), chalk.white('Fila de processamento iniciada'));
|
|
123
|
+
|
|
124
|
+
if (this.queue.length > 0 && !this.isProcessing) {
|
|
125
|
+
this.processQueue();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
stop() {
|
|
130
|
+
this.isRunning = false;
|
|
131
|
+
console.log(chalk.yellow('⏸'), chalk.white('Fila de processamento pausada'));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
clear() {
|
|
135
|
+
const count = this.queue.length;
|
|
136
|
+
this.queue = [];
|
|
137
|
+
console.log(chalk.yellow('🗑'), chalk.white(`Fila limpa: ${count} itens removidos`));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
getQueueSize() {
|
|
141
|
+
return this.queue.length;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
getStats() {
|
|
145
|
+
return {
|
|
146
|
+
...this.stats,
|
|
147
|
+
queueSize: this.queue.length,
|
|
148
|
+
isProcessing: this.isProcessing,
|
|
149
|
+
isRunning: this.isRunning
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = RequestQueue;
|
package/src/sanitizer.js
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
const SENSITIVE_KEY_RE = [
|
|
2
|
+
/passw(or)?d/i,
|
|
3
|
+
/\bsecret\b/i,
|
|
4
|
+
/\btoken\b/i,
|
|
5
|
+
/api[_-]?key/i,
|
|
6
|
+
/\bauth\b/i,
|
|
7
|
+
/\bcredential/i,
|
|
8
|
+
/session[_-]?id/i,
|
|
9
|
+
/private[_-]?key/i,
|
|
10
|
+
/client[_-]?secret/i,
|
|
11
|
+
/\bssn\b/i,
|
|
12
|
+
/credit[_-]?card/i,
|
|
13
|
+
/card[_-]?number/i,
|
|
14
|
+
/\bcvv\b/i,
|
|
15
|
+
/\bcvc\b/i,
|
|
16
|
+
/\bpin\b/i,
|
|
17
|
+
/\bcpf\b/i,
|
|
18
|
+
/\bcnpj\b/i,
|
|
19
|
+
/\bsalt\b/i,
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const SAFE_HEADERS = new Set([
|
|
23
|
+
'content-type', 'accept', 'accept-language', 'accept-encoding',
|
|
24
|
+
'cache-control', 'connection', 'host', 'origin', 'referer',
|
|
25
|
+
'user-agent', 'content-length', 'content-encoding',
|
|
26
|
+
'access-control-allow-origin', 'access-control-allow-methods',
|
|
27
|
+
'access-control-allow-headers', 'access-control-allow-credentials',
|
|
28
|
+
'access-control-max-age', 'access-control-expose-headers',
|
|
29
|
+
'vary', 'x-powered-by', 'x-request-id', 'x-correlation-id',
|
|
30
|
+
'transfer-encoding', 'etag', 'last-modified', 'if-none-match',
|
|
31
|
+
'if-modified-since', 'x-ratelimit-limit', 'x-ratelimit-remaining',
|
|
32
|
+
'x-ratelimit-reset', 'retry-after', 'server',
|
|
33
|
+
'date', 'expires', 'pragma', 'x-content-type-options',
|
|
34
|
+
'x-frame-options', 'strict-transport-security',
|
|
35
|
+
'content-security-policy', 'x-xss-protection',
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const SENSITIVE_HEADER_NAMES = new Set([
|
|
39
|
+
'x-api-key', 'api-key', 'apikey',
|
|
40
|
+
'x-api-token', 'api-token', 'apitoken',
|
|
41
|
+
'x-auth-token', 'access-token', 'x-access-token',
|
|
42
|
+
'x-csrf-token', 'x-xsrf-token',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
const SENSITIVE_PARAMS = new Set([
|
|
46
|
+
'api_key', 'apikey', 'api-key', 'key',
|
|
47
|
+
'token', 'access_token', 'auth_token', 'refresh_token',
|
|
48
|
+
'secret', 'password', 'passwd', 'session', 'sid',
|
|
49
|
+
'client_secret',
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
const VP = {
|
|
53
|
+
email: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
|
|
54
|
+
jwt: /^eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]+$/,
|
|
55
|
+
uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
56
|
+
phone: /^[+]?[(]?[0-9]{1,4}[)]?[-\s.]?[0-9]{1,4}[-\s.]?[0-9]{1,9}$/,
|
|
57
|
+
creditCard: /^\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}$/,
|
|
58
|
+
ipv4: /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/,
|
|
59
|
+
hexToken: /^[0-9a-f]{32,}$/i,
|
|
60
|
+
base64Long: /^[A-Za-z0-9+/]{20,}={0,2}$/,
|
|
61
|
+
isoDate: /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2})?/,
|
|
62
|
+
url: /^https?:\/\/.+/i,
|
|
63
|
+
bearer: /^Bearer\s+(.+)$/i,
|
|
64
|
+
basic: /^Basic\s+(.+)$/i,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const INTERCEPTOR_MARKERS = /^\[(File:|Binary Data:|String too large|Response too large|FormData|Binary Content|Error)/;
|
|
68
|
+
|
|
69
|
+
function isSensitiveKey(key) {
|
|
70
|
+
if (!key || typeof key !== 'string') return false;
|
|
71
|
+
return SENSITIVE_KEY_RE.some(re => re.test(key));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function smartPlaceholder(value) {
|
|
75
|
+
if (value === null || value === undefined) return value;
|
|
76
|
+
if (typeof value === 'boolean') return value;
|
|
77
|
+
|
|
78
|
+
if (typeof value === 'number') {
|
|
79
|
+
if (Number.isInteger(value)) {
|
|
80
|
+
if (value === 0) return 0;
|
|
81
|
+
const sign = value < 0 ? -1 : 1;
|
|
82
|
+
const digits = String(Math.abs(value)).length;
|
|
83
|
+
if (digits === 1) return sign * (Math.floor(Math.random() * 9) + 1);
|
|
84
|
+
const min = Math.pow(10, digits - 1);
|
|
85
|
+
const max = Math.pow(10, digits) - 1;
|
|
86
|
+
return sign * (Math.floor(Math.random() * (max - min + 1)) + min);
|
|
87
|
+
}
|
|
88
|
+
return Math.round(Math.random() * 100 * 100) / 100;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (typeof value !== 'string') return value;
|
|
92
|
+
if (value.length === 0) return '';
|
|
93
|
+
|
|
94
|
+
if (INTERCEPTOR_MARKERS.test(value)) return value;
|
|
95
|
+
|
|
96
|
+
if (VP.email.test(value)) return 'user@example.com';
|
|
97
|
+
if (VP.jwt.test(value)) return 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.REDACTED';
|
|
98
|
+
if (VP.uuid.test(value)) return '550e8400-e29b-41d4-a716-446655440000';
|
|
99
|
+
if (VP.creditCard.test(value)) return '4111 1111 1111 1111';
|
|
100
|
+
if (VP.phone.test(value)) return '+1 555-000-0000';
|
|
101
|
+
if (VP.ipv4.test(value)) return '192.168.1.1';
|
|
102
|
+
if (VP.isoDate.test(value)) return '2025-01-01T00:00:00.000Z';
|
|
103
|
+
if (VP.url.test(value)) return 'https://example.com/path';
|
|
104
|
+
if (VP.hexToken.test(value)) return 'a'.repeat(Math.min(value.length, 40));
|
|
105
|
+
if (VP.base64Long.test(value) && value.length > 30) return '[REDACTED_TOKEN]';
|
|
106
|
+
|
|
107
|
+
if (value.length <= 5) return 'text';
|
|
108
|
+
if (value.length <= 20) return 'string_value';
|
|
109
|
+
return 'lorem ipsum dolor sit amet';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function redactForKey(key) {
|
|
113
|
+
const k = (key || '').toLowerCase();
|
|
114
|
+
if (k.includes('password') || k.includes('passwd')) return '[REDACTED_PASSWORD]';
|
|
115
|
+
if (k.includes('token')) return '[REDACTED_TOKEN]';
|
|
116
|
+
if (k.includes('secret')) return '[REDACTED_SECRET]';
|
|
117
|
+
if (k.includes('key')) return '[REDACTED_KEY]';
|
|
118
|
+
if (k.includes('credential')) return '[REDACTED_CREDENTIAL]';
|
|
119
|
+
if (k.includes('session')) return '[REDACTED_SESSION]';
|
|
120
|
+
if (k.includes('cpf')) return '[REDACTED_CPF]';
|
|
121
|
+
if (k.includes('cnpj')) return '[REDACTED_CNPJ]';
|
|
122
|
+
return '[REDACTED]';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function sanitizeBody(data, depth = 0) {
|
|
126
|
+
if (depth > 20) return data;
|
|
127
|
+
|
|
128
|
+
if (data === null || data === undefined) return data;
|
|
129
|
+
if (typeof data === 'boolean') return data;
|
|
130
|
+
if (typeof data === 'number') return smartPlaceholder(data);
|
|
131
|
+
if (typeof data === 'string') return smartPlaceholder(data);
|
|
132
|
+
|
|
133
|
+
if (Array.isArray(data)) {
|
|
134
|
+
return data.map(item => sanitizeBody(item, depth + 1));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (typeof data === 'object') {
|
|
138
|
+
const result = {};
|
|
139
|
+
for (const key in data) {
|
|
140
|
+
if (!Object.prototype.hasOwnProperty.call(data, key)) continue;
|
|
141
|
+
if (isSensitiveKey(key)) {
|
|
142
|
+
result[key] = redactForKey(key);
|
|
143
|
+
} else {
|
|
144
|
+
result[key] = sanitizeBody(data[key], depth + 1);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return data;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function sanitizeCookieHeader(cookieStr) {
|
|
154
|
+
if (!cookieStr || typeof cookieStr !== 'string') return cookieStr;
|
|
155
|
+
|
|
156
|
+
return cookieStr
|
|
157
|
+
.split(';')
|
|
158
|
+
.map((part) => {
|
|
159
|
+
const eqIndex = part.indexOf('=');
|
|
160
|
+
if (eqIndex === -1) return part;
|
|
161
|
+
const name = part.substring(0, eqIndex);
|
|
162
|
+
return name + '=[REDACTED_COOKIE]';
|
|
163
|
+
})
|
|
164
|
+
.join('; ');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function sanitizeSetCookieHeader(value) {
|
|
168
|
+
if (!value || typeof value !== 'string') return value;
|
|
169
|
+
const parts = value.split(';');
|
|
170
|
+
if (parts.length > 0) {
|
|
171
|
+
const eqIndex = parts[0].indexOf('=');
|
|
172
|
+
if (eqIndex !== -1) {
|
|
173
|
+
const name = parts[0].substring(0, eqIndex);
|
|
174
|
+
parts[0] = name + '=[REDACTED_COOKIE]';
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return parts.join(';');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function sanitizeHeaders(headers) {
|
|
181
|
+
if (!headers || typeof headers !== 'object') return headers;
|
|
182
|
+
|
|
183
|
+
const sanitized = {};
|
|
184
|
+
|
|
185
|
+
for (const key in headers) {
|
|
186
|
+
if (!Object.prototype.hasOwnProperty.call(headers, key)) continue;
|
|
187
|
+
const value = headers[key];
|
|
188
|
+
const lower = String(key).toLowerCase();
|
|
189
|
+
|
|
190
|
+
if (SAFE_HEADERS.has(lower)) {
|
|
191
|
+
sanitized[key] = value;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (lower === 'authorization' || lower === 'proxy-authorization') {
|
|
196
|
+
if (typeof value === 'string') {
|
|
197
|
+
if (VP.bearer.test(value)) {
|
|
198
|
+
const token = value.replace(VP.bearer, '$1');
|
|
199
|
+
sanitized[key] = 'Bearer ' + smartPlaceholder(token);
|
|
200
|
+
} else if (VP.basic.test(value)) {
|
|
201
|
+
sanitized[key] = 'Basic [REDACTED]';
|
|
202
|
+
} else {
|
|
203
|
+
sanitized[key] = '[REDACTED]';
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
sanitized[key] = '[REDACTED]';
|
|
207
|
+
}
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (lower === 'cookie') {
|
|
212
|
+
sanitized[key] = sanitizeCookieHeader(value);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (lower === 'set-cookie') {
|
|
217
|
+
sanitized[key] = Array.isArray(value)
|
|
218
|
+
? value.map(v => sanitizeSetCookieHeader(v))
|
|
219
|
+
: sanitizeSetCookieHeader(value);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (SENSITIVE_HEADER_NAMES.has(lower)) {
|
|
224
|
+
sanitized[key] = '[REDACTED]';
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
sanitized[key] = value;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return sanitized;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function sanitizeUrl(originalUrl) {
|
|
235
|
+
if (!originalUrl || typeof originalUrl !== 'string') return originalUrl;
|
|
236
|
+
|
|
237
|
+
const qIndex = originalUrl.indexOf('?');
|
|
238
|
+
if (qIndex === -1) return originalUrl;
|
|
239
|
+
|
|
240
|
+
const path = originalUrl.substring(0, qIndex);
|
|
241
|
+
const rest = originalUrl.substring(qIndex + 1);
|
|
242
|
+
const hashIndex = rest.indexOf('#');
|
|
243
|
+
const rawQuery = hashIndex !== -1 ? rest.substring(0, hashIndex) : rest;
|
|
244
|
+
const hash = hashIndex !== -1 ? rest.substring(hashIndex) : '';
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const params = new URLSearchParams(rawQuery);
|
|
248
|
+
const sanitizedParts = [];
|
|
249
|
+
|
|
250
|
+
params.forEach((value, key) => {
|
|
251
|
+
if (SENSITIVE_PARAMS.has(String(key).toLowerCase()) || isSensitiveKey(key)) {
|
|
252
|
+
sanitizedParts.push(encodeURIComponent(key) + '=' + encodeURIComponent('[REDACTED]'));
|
|
253
|
+
} else {
|
|
254
|
+
sanitizedParts.push(encodeURIComponent(key) + '=' + encodeURIComponent(smartPlaceholder(value)));
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const newQuery = sanitizedParts.join('&');
|
|
259
|
+
return path + (newQuery ? '?' + newQuery : '') + hash;
|
|
260
|
+
} catch (_e) {
|
|
261
|
+
return originalUrl;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function sanitizeRequestData(requestData) {
|
|
266
|
+
if (!requestData) return requestData;
|
|
267
|
+
|
|
268
|
+
const sanitized = {};
|
|
269
|
+
|
|
270
|
+
if (requestData.req) {
|
|
271
|
+
sanitized.req = {
|
|
272
|
+
method: requestData.req.method,
|
|
273
|
+
protocol: requestData.req.protocol,
|
|
274
|
+
headers: sanitizeHeaders(requestData.req.headers),
|
|
275
|
+
originalUrl: sanitizeUrl(requestData.req.originalUrl),
|
|
276
|
+
body: requestData.req.body != null ? sanitizeBody(requestData.req.body) : requestData.req.body,
|
|
277
|
+
socket: requestData.req.socket,
|
|
278
|
+
security: requestData.req.security,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (requestData.res) {
|
|
283
|
+
sanitized.res = {
|
|
284
|
+
statusCode: requestData.res.statusCode,
|
|
285
|
+
statusMessage: requestData.res.statusMessage,
|
|
286
|
+
headers: sanitizeHeaders(requestData.res.headers),
|
|
287
|
+
body: requestData.res.body != null ? sanitizeBody(requestData.res.body) : requestData.res.body,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (requestData.timestamp) {
|
|
292
|
+
sanitized.timestamp = requestData.timestamp;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return sanitized;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
module.exports = {
|
|
299
|
+
sanitizeRequestData,
|
|
300
|
+
sanitizeHeaders,
|
|
301
|
+
sanitizeBody,
|
|
302
|
+
sanitizeUrl,
|
|
303
|
+
};
|