localhostly 1.0.0 → 1.1.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/README.md +6 -1
- package/agente/cli.js +24 -3
- package/agente/servico/comum.js +4 -2
- package/agente/servico/index.js +6 -4
- package/agente/tela.js +8 -3
- package/agente/tunel.js +21 -0
- package/lib/permitidos.js +85 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -35,6 +35,7 @@ localhostly 3000 # expose http://127.0.0.1:3000
|
|
|
35
35
|
localhostly http 3000 --password # visitors must type a password (a random one is shown)
|
|
36
36
|
localhostly http 3000 --password s3cret # ...or your own
|
|
37
37
|
localhostly http 8080 --host myapp.test # forward to another local address; the app receives it as Host
|
|
38
|
+
localhostly http 3000 --allow 203.0.113.7,10.0.0.0/8 # only these IP addresses or ranges get in; others see 403
|
|
38
39
|
```
|
|
39
40
|
|
|
40
41
|
The terminal shows the public URL and every request live. HTTP, streaming responses and WebSockets all go through.
|
|
@@ -43,6 +44,10 @@ Stop with Ctrl+C.
|
|
|
43
44
|
The first time a browser opens your URL it sees a short warning page (it protects against phishing); API clients,
|
|
44
45
|
webhooks and `curl` never see it, and a browser can skip it by sending the header `X-Localhostly-Skip-Warning`.
|
|
45
46
|
|
|
47
|
+
With `--allow` (IPv4 or IPv6 addresses and CIDR ranges, comma-separated or repeated, up to 64) only those
|
|
48
|
+
addresses reach your app, without the warning page; every other address gets a 403 page that shows it, WebSockets
|
|
49
|
+
included. `--allow` and `--password` together require both. `localhostly service install` takes `--allow` too.
|
|
50
|
+
|
|
46
51
|
## Keep it always on
|
|
47
52
|
|
|
48
53
|
```sh
|
|
@@ -56,7 +61,7 @@ with `npm i -g` first — a copy run by `npx` lives in a temporary folder.
|
|
|
56
61
|
|
|
57
62
|
## Free plan limits
|
|
58
63
|
|
|
59
|
-
2 tunnels at a time,
|
|
64
|
+
2 tunnels at a time, 1000 requests per minute and 25 GiB per month per account.
|
|
60
65
|
|
|
61
66
|
## All options
|
|
62
67
|
|
package/agente/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import { randomInt } from 'node:crypto';
|
|
|
4
4
|
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { join, resolve } from 'node:path';
|
|
7
|
+
import { ErroLista, normalizarLista, textoDaLista } from '../lib/permitidos.js';
|
|
7
8
|
import * as servico from './servico/index.js';
|
|
8
9
|
import { criarLog, criarTela } from './tela.js';
|
|
9
10
|
import { SERVIDOR_PADRAO, Tunel } from './tunel.js';
|
|
@@ -30,6 +31,8 @@ Options:
|
|
|
30
31
|
--password [value] ask visitors for a password (the browser shows a login box; any user name);
|
|
31
32
|
without a value, a random one is created and shown here
|
|
32
33
|
--password-file <file> the same, with the password read from a file
|
|
34
|
+
--allow <ip[,ip|cidr]> only these IP addresses or ranges can reach the tunnel; everyone else gets 403
|
|
35
|
+
(comma-separated, e.g. 203.0.113.7,10.0.0.0/8; use more than once to add)
|
|
33
36
|
--service service mode: one log line per event, no live table, never gives up
|
|
34
37
|
--home <dir> where the token and the service state live (default ~/.localhostly,
|
|
35
38
|
or $LOCALHOSTLY_HOME)
|
|
@@ -90,6 +93,7 @@ export function lerAlvo(texto, portaPadrao) {
|
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
export function lerArgumentos(argv) {
|
|
96
|
+
const allow = [];
|
|
93
97
|
const opcoes = {
|
|
94
98
|
comando: null,
|
|
95
99
|
posicionais: [],
|
|
@@ -97,6 +101,7 @@ export function lerArgumentos(argv) {
|
|
|
97
101
|
servidor: null,
|
|
98
102
|
senha: false,
|
|
99
103
|
arquivoSenha: null,
|
|
104
|
+
allow: null, // a lista normalizada (textos), ou null sem --allow
|
|
100
105
|
servico: false,
|
|
101
106
|
home: null,
|
|
102
107
|
};
|
|
@@ -128,11 +133,25 @@ export function lerArgumentos(argv) {
|
|
|
128
133
|
else if (chave === '--server') opcoes.servidor = valorDe();
|
|
129
134
|
else if (chave === '--password') opcoes.senha = valorDaSenha();
|
|
130
135
|
else if (chave === '--password-file') opcoes.arquivoSenha = valorDe();
|
|
136
|
+
else if (chave === '--allow') {
|
|
137
|
+
const texto = valorDe();
|
|
138
|
+
if (!texto.trim()) throw erroDeUso(`${chave} needs a value: an IP address or range, e.g. --allow 203.0.113.7`);
|
|
139
|
+
allow.push(texto);
|
|
140
|
+
}
|
|
131
141
|
else if (chave === '--service') opcoes.servico = true;
|
|
132
142
|
else if (chave === '--home') opcoes.home = valorDe();
|
|
133
143
|
else if (chave.startsWith('-')) throw erroDeUso(`unknown option: ${chave}`);
|
|
134
144
|
else opcoes.posicionais.push(a);
|
|
135
145
|
}
|
|
146
|
+
if (allow.length) {
|
|
147
|
+
try {
|
|
148
|
+
opcoes.allow = textoDaLista(normalizarLista(allow));
|
|
149
|
+
} catch (erro) {
|
|
150
|
+
if (!(erro instanceof ErroLista)) throw erro;
|
|
151
|
+
throw erroDeUso(`invalid --allow: ${erro.message} (use IP addresses or ranges, e.g. --allow 203.0.113.7,10.0.0.0/8)`);
|
|
152
|
+
}
|
|
153
|
+
if (!opcoes.allow.length) throw erroDeUso('--allow needs at least one IP address or range');
|
|
154
|
+
}
|
|
136
155
|
if (!opcoes.comando) {
|
|
137
156
|
const [primeiro] = opcoes.posicionais;
|
|
138
157
|
if (COMANDOS.includes(primeiro)) opcoes.comando = opcoes.posicionais.shift();
|
|
@@ -169,7 +188,7 @@ function alvoDe(opcoes, uso) {
|
|
|
169
188
|
}
|
|
170
189
|
|
|
171
190
|
const USO_SERVICO =
|
|
172
|
-
'usage: localhostly service install <port> [--host h[:p]] [--password [value]] [--server url]\n' +
|
|
191
|
+
'usage: localhostly service install <port> [--host h[:p]] [--password [value]] [--allow ip[,ip|cidr]] [--server url]\n' +
|
|
173
192
|
' localhostly service status\n' +
|
|
174
193
|
' localhostly service uninstall';
|
|
175
194
|
|
|
@@ -204,11 +223,12 @@ export async function comandoServico(
|
|
|
204
223
|
}
|
|
205
224
|
const servidor = opcoes.servidor || env.LOCALHOSTLY_SERVER || config.server || null;
|
|
206
225
|
const antes = Date.now();
|
|
207
|
-
const r = api.instalar({ porta, host: opcoes.host, servidor, senha, home }, { plataforma, env });
|
|
226
|
+
const r = api.instalar({ porta, host: opcoes.host, servidor, senha, allow: opcoes.allow, home }, { plataforma, env });
|
|
208
227
|
saida.write(`Installed the localhostly ${nome}: ${r.arquivo}\n`);
|
|
209
228
|
saida.write('It starts with the computer, reconnects by itself and keeps the same name.\n');
|
|
210
229
|
if (gerada) saida.write(`Password: ${senha} (any user name)\n`);
|
|
211
230
|
else if (senha) saida.write('Password protected (any user name).\n');
|
|
231
|
+
if (opcoes.allow) saida.write(`Allowed: ${opcoes.allow.join(', ')} (other addresses get 403)\n`);
|
|
212
232
|
const fim = Date.now() + esperarUrlMs;
|
|
213
233
|
let tunel = null;
|
|
214
234
|
while (Date.now() < fim) {
|
|
@@ -306,6 +326,7 @@ export async function principal(argv = process.argv.slice(2), { env = process.en
|
|
|
306
326
|
alvo,
|
|
307
327
|
hostCabecalho: opcoes.host,
|
|
308
328
|
senha,
|
|
329
|
+
allow: opcoes.allow,
|
|
309
330
|
sessao: modoServico ? servico.lerEstado(home) : null,
|
|
310
331
|
insistir: modoServico,
|
|
311
332
|
});
|
|
@@ -318,7 +339,7 @@ export async function principal(argv = process.argv.slice(2), { env = process.en
|
|
|
318
339
|
tela.aviso(`could not save the tunnel state in ${home}: ${erro.code ?? erro.message}`);
|
|
319
340
|
}
|
|
320
341
|
}
|
|
321
|
-
tela.pronto({ url, alvo: tunel.enderecoLocal, senha: senha && (gerada ? senha : true), retomado });
|
|
342
|
+
tela.pronto({ url, alvo: tunel.enderecoLocal, senha: senha && (gerada ? senha : true), retomado, permitidos: opcoes.allow });
|
|
322
343
|
});
|
|
323
344
|
tunel.on('nome_trocado', ({ de, para }) =>
|
|
324
345
|
tela.aviso(`name changed: ${de} -> ${para} (the old name was released while the tunnel was offline)`),
|
package/agente/servico/comum.js
CHANGED
|
@@ -14,14 +14,16 @@ export function executar(programa, args = [], opcoes = {}) {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
// O argv do agente que o serviço roda: [node, bin/localhostly.js, 'http', porta, '--service', ...]. A senha nunca vai
|
|
17
|
-
// na linha de comando (o `ps` a mostraria a qualquer usuário): só o caminho do arquivo 0600 que a guarda.
|
|
18
|
-
|
|
17
|
+
// na linha de comando (o `ps` a mostraria a qualquer usuário): só o caminho do arquivo 0600 que a guarda. A lista
|
|
18
|
+
// do --allow (não é segredo) vai como "--allow a,b".
|
|
19
|
+
export function comandoDoAgente({ exe, porta, host = null, servidor = null, home, arquivoSenha = null, allow = null }) {
|
|
19
20
|
if (!Array.isArray(exe) || exe.length === 0) throw new Error('exe: the agent command is missing');
|
|
20
21
|
if (!Number.isInteger(porta) || porta < 1 || porta > 65535) throw new Error(`invalid port: ${porta}`);
|
|
21
22
|
const args = [...exe, 'http', String(porta), '--service', '--home', home];
|
|
22
23
|
if (host) args.push('--host', host);
|
|
23
24
|
if (servidor) args.push('--server', servidor);
|
|
24
25
|
if (arquivoSenha) args.push('--password-file', arquivoSenha);
|
|
26
|
+
if (allow?.length) args.push('--allow', allow.join(','));
|
|
25
27
|
return args;
|
|
26
28
|
}
|
|
27
29
|
|
package/agente/servico/index.js
CHANGED
|
@@ -61,11 +61,13 @@ export function gravarEstado(home, { nome, tokenSessao, url }, agora = new Date(
|
|
|
61
61
|
return arquivo;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
// opcoes: { porta, host, servidor, senha (texto ou null), home, exe }
|
|
64
|
+
// opcoes: { porta, host, servidor, senha (texto ou null), allow (a lista normalizada do --allow, ou null), home, exe }
|
|
65
65
|
export function instalar(opcoes, { plataforma = process.platform, casa = homedir(), env = process.env, exec = executar, uid } = {}) {
|
|
66
66
|
const s = sistema(plataforma);
|
|
67
|
-
const { porta, host = null, servidor = null, senha = null, home, exe = exePadrao() } = opcoes;
|
|
68
|
-
if (!Number.isInteger(porta))
|
|
67
|
+
const { porta, host = null, servidor = null, senha = null, allow = null, home, exe = exePadrao() } = opcoes;
|
|
68
|
+
if (!Number.isInteger(porta)) {
|
|
69
|
+
throw new ErroServico('usage: localhostly service install <port> [--host h[:p]] [--password [value]] [--allow ip[,ip|cidr]]');
|
|
70
|
+
}
|
|
69
71
|
mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
70
72
|
let arquivo = null;
|
|
71
73
|
if (senha) {
|
|
@@ -77,7 +79,7 @@ export function instalar(opcoes, { plataforma = process.platform, casa = homedir
|
|
|
77
79
|
} else {
|
|
78
80
|
rmSync(arquivoSenha(home), { force: true });
|
|
79
81
|
}
|
|
80
|
-
const comando = comandoDoAgente({ exe, porta, host, servidor, home, arquivoSenha: arquivo });
|
|
82
|
+
const comando = comandoDoAgente({ exe, porta, host, servidor, home, arquivoSenha: arquivo, allow });
|
|
81
83
|
const r = s.instalar({ comando, porta, home }, { casa, env, exec, home, ...(uid === undefined ? {} : { uid }) });
|
|
82
84
|
// o npx roda de uma pasta de cache que pode sumir: o serviço apontaria para um caminho morto
|
|
83
85
|
if (exe.some((parte) => /[\\/]_npx[\\/]/.test(parte))) {
|
package/agente/tela.js
CHANGED
|
@@ -21,9 +21,11 @@ export function criarTela(saida = process.stdout, { cor = Boolean(saida.isTTY) &
|
|
|
21
21
|
linha(pintar('cinza', `connecting to ${servidor}…`));
|
|
22
22
|
},
|
|
23
23
|
// senha: o texto (a gerada, que só esta tela mostra), true (a que o usuário escolheu) ou nada.
|
|
24
|
-
|
|
24
|
+
// permitidos: a lista do --allow (só esses endereços entram) ou nada.
|
|
25
|
+
pronto({ url, alvo, senha = null, permitidos = null }) {
|
|
25
26
|
linha('');
|
|
26
27
|
linha(`${pintar('negrito', url)} → http://${alvo}`);
|
|
28
|
+
if (permitidos?.length) linha(`Allowed: ${pintar('negrito', permitidos.join(', '))} ${pintar('cinza', '(other addresses get 403)')}`);
|
|
27
29
|
if (typeof senha === 'string') linha(`Password: ${pintar('negrito', senha)} ${pintar('cinza', '(any user name)')}`);
|
|
28
30
|
else if (senha) linha(pintar('cinza', 'Password protected (any user name).'));
|
|
29
31
|
linha(pintar('cinza', 'Ctrl+C to stop. Requests:'));
|
|
@@ -53,8 +55,11 @@ export function criarLog(saida = process.stdout, { relogio = () => new Date() }
|
|
|
53
55
|
conectando(servidor) {
|
|
54
56
|
linha(`connecting to ${servidor}`);
|
|
55
57
|
},
|
|
56
|
-
pronto({ url, alvo, senha = null, retomado = false }) {
|
|
57
|
-
linha(
|
|
58
|
+
pronto({ url, alvo, senha = null, retomado = false, permitidos = null }) {
|
|
59
|
+
linha(
|
|
60
|
+
`online ${url} -> http://${alvo}${retomado ? ' (same name)' : ''}${senha ? ' (password protected)' : ''}` +
|
|
61
|
+
`${permitidos?.length ? ` (allowed: ${permitidos.join(',')})` : ''}`,
|
|
62
|
+
);
|
|
58
63
|
},
|
|
59
64
|
requisicao() {},
|
|
60
65
|
aviso(texto) {
|
package/agente/tunel.js
CHANGED
|
@@ -29,6 +29,10 @@ function paginaErro(alvo, erro) {
|
|
|
29
29
|
);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function mesmaLista(a, b) {
|
|
33
|
+
return Array.isArray(a) && a.length === b.length && a.every((e, i) => e === b[i]);
|
|
34
|
+
}
|
|
35
|
+
|
|
32
36
|
// alvo: { host, porta } do app local; hostCabecalho: o Host que o app recebe (null = o original).
|
|
33
37
|
export class Tunel extends EventEmitter {
|
|
34
38
|
constructor({
|
|
@@ -37,6 +41,8 @@ export class Tunel extends EventEmitter {
|
|
|
37
41
|
alvo,
|
|
38
42
|
hostCabecalho = null,
|
|
39
43
|
senha = null,
|
|
44
|
+
// a lista do --allow (textos normalizados por lib/permitidos.js), ou null: o túnel aberto a todos
|
|
45
|
+
allow = null,
|
|
40
46
|
reconectar = true,
|
|
41
47
|
recuoMin = RECUO_MIN,
|
|
42
48
|
recuoMax = RECUO_MAX,
|
|
@@ -53,6 +59,7 @@ export class Tunel extends EventEmitter {
|
|
|
53
59
|
this.alvo = alvo;
|
|
54
60
|
this.hostCabecalho = hostCabecalho;
|
|
55
61
|
this.senha = senha;
|
|
62
|
+
this.allow = Array.isArray(allow) && allow.length ? [...allow] : null;
|
|
56
63
|
this.reconectar = reconectar;
|
|
57
64
|
this.silencioMs = silencioMs;
|
|
58
65
|
this.handshakeMs = handshakeMs;
|
|
@@ -114,6 +121,7 @@ export class Tunel extends EventEmitter {
|
|
|
114
121
|
this.ws = ws;
|
|
115
122
|
const canal = new Canal(ws);
|
|
116
123
|
let recusado = false;
|
|
124
|
+
let aceito = false; // o "ready" valeu: só então as requisições chegam ao app local
|
|
117
125
|
const pedido = this.sessao?.nome ?? null;
|
|
118
126
|
let silencio = null;
|
|
119
127
|
const vigiar = () => {
|
|
@@ -130,6 +138,7 @@ export class Tunel extends EventEmitter {
|
|
|
130
138
|
tokenSessao: this.sessao?.tokenSessao ?? null,
|
|
131
139
|
host: this.hostCabecalho,
|
|
132
140
|
password: this.senha,
|
|
141
|
+
allow: this.allow,
|
|
133
142
|
});
|
|
134
143
|
});
|
|
135
144
|
ws.on('ping', vigiar);
|
|
@@ -138,6 +147,17 @@ export class Tunel extends EventEmitter {
|
|
|
138
147
|
canal.on('invalido', () => ws.terminate());
|
|
139
148
|
canal.on('controle', (msg) => {
|
|
140
149
|
if (msg.t === 'ready') {
|
|
150
|
+
// pediu --allow e o servidor não devolveu a mesma lista (o servidor de antes ignora o campo): o túnel
|
|
151
|
+
// estaria aberto a todos, então fecha em vez de seguir
|
|
152
|
+
if (this.allow && !mesmaLista(msg.allow, this.allow)) {
|
|
153
|
+
recusado = true;
|
|
154
|
+
const erro = new Error('this server does not support --allow; update the server or remove --allow');
|
|
155
|
+
erro.codigo = 'allow_unsupported';
|
|
156
|
+
this.emit('recusado', erro);
|
|
157
|
+
ws.terminate();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
aceito = true;
|
|
141
161
|
this.tentativas = 0;
|
|
142
162
|
this.nome = msg.nome;
|
|
143
163
|
this.url = msg.url;
|
|
@@ -158,6 +178,7 @@ export class Tunel extends EventEmitter {
|
|
|
158
178
|
}
|
|
159
179
|
});
|
|
160
180
|
canal.aoAbrir = (quadro) => {
|
|
181
|
+
if (!aceito) return;
|
|
161
182
|
if (quadro.tipo === TIPO.REQ_INICIO) this.#requisicao(canal, quadro);
|
|
162
183
|
else this.#websocket(canal, quadro);
|
|
163
184
|
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// A lista de endereços que podem visitar o túnel (--allow): IPs e faixas (CIDR), IPv4 e IPv6. O agente a confere
|
|
2
|
+
// antes de abrir e o servidor de novo no hello; os dois normalizam pela mesma regra, então o eco do "ready" é igual
|
|
3
|
+
// à lista que o agente mandou.
|
|
4
|
+
import net from 'node:net';
|
|
5
|
+
|
|
6
|
+
export const MAX_PERMITIDOS = 64;
|
|
7
|
+
|
|
8
|
+
export class ErroLista extends Error {
|
|
9
|
+
constructor(mensagem, entrada) {
|
|
10
|
+
super(mensagem);
|
|
11
|
+
this.entrada = entrada;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// "::ffff:1.2.3.4" (IPv4 mapeado em IPv6, como o socket de dupla pilha mostra) → "1.2.3.4".
|
|
16
|
+
function semMapeado(ip) {
|
|
17
|
+
const m = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/i.exec(ip);
|
|
18
|
+
return m && net.isIPv4(m[1]) ? m[1] : ip;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Um endereço na forma canônica: { ip, familia: 'ipv4' | 'ipv6' } ou null.
|
|
22
|
+
export function lerIp(texto) {
|
|
23
|
+
const ip = semMapeado(String(texto ?? '').trim());
|
|
24
|
+
const versao = net.isIP(ip);
|
|
25
|
+
if (versao === 4) return { ip, familia: 'ipv4' };
|
|
26
|
+
if (versao === 6) return { ip: new net.SocketAddress({ address: ip, family: 'ipv6' }).address, familia: 'ipv6' };
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Uma entrada: "203.0.113.7", "10.0.0.0/8", "2001:db8::/32" → { ip, bits, familia, texto }; inválida → null.
|
|
31
|
+
export function lerEntrada(texto) {
|
|
32
|
+
const t = String(texto ?? '').trim();
|
|
33
|
+
const m = /^([^/]+)(?:\/(\d{1,3}))?$/.exec(t);
|
|
34
|
+
if (!m) return null;
|
|
35
|
+
const end = lerIp(m[1]);
|
|
36
|
+
if (!end) return null;
|
|
37
|
+
const max = end.familia === 'ipv4' ? 32 : 128;
|
|
38
|
+
// o mapeado com prefixo de IPv6 (/96 + n) vira o prefixo de IPv4
|
|
39
|
+
let bits = m[2] === undefined ? max : Number(m[2]);
|
|
40
|
+
if (m[2] !== undefined && end.familia === 'ipv4' && m[1].includes(':')) bits -= 96;
|
|
41
|
+
if (!Number.isInteger(bits) || bits < 0 || bits > max) return null;
|
|
42
|
+
return { ip: end.ip, bits, familia: end.familia, texto: bits === max ? end.ip : `${end.ip}/${bits}` };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// texto ("a,b") ou lista de textos (cada um pode ter vírgulas) → a lista sem repetidas, na ordem; vazia → [].
|
|
46
|
+
// Lança ErroLista com a primeira entrada inválida ou com mais de MAX_PERMITIDOS.
|
|
47
|
+
export function normalizarLista(valor) {
|
|
48
|
+
const pedacos = (Array.isArray(valor) ? valor : [valor])
|
|
49
|
+
.flatMap((v) => String(v ?? '').split(','))
|
|
50
|
+
.map((p) => p.trim())
|
|
51
|
+
.filter(Boolean);
|
|
52
|
+
const lista = [];
|
|
53
|
+
const vistos = new Set();
|
|
54
|
+
for (const p of pedacos) {
|
|
55
|
+
const e = lerEntrada(p);
|
|
56
|
+
if (!e) throw new ErroLista(`not an IP address or range: ${p}`, p);
|
|
57
|
+
if (vistos.has(e.texto)) continue;
|
|
58
|
+
vistos.add(e.texto);
|
|
59
|
+
lista.push(e);
|
|
60
|
+
}
|
|
61
|
+
if (lista.length > MAX_PERMITIDOS) throw new ErroLista(`too many addresses: ${lista.length} (at most ${MAX_PERMITIDOS})`, null);
|
|
62
|
+
return lista;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function textoDaLista(lista) {
|
|
66
|
+
return (lista ?? []).map((e) => e.texto);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// O conferidor de uma lista normalizada: (ip) → true se o endereço está nela.
|
|
70
|
+
export function criarConferidor(lista) {
|
|
71
|
+
const bloco = new net.BlockList();
|
|
72
|
+
for (const e of lista) {
|
|
73
|
+
const max = e.familia === 'ipv4' ? 32 : 128;
|
|
74
|
+
if (e.bits === max) bloco.addAddress(e.ip, e.familia);
|
|
75
|
+
else bloco.addSubnet(e.ip, e.bits, e.familia);
|
|
76
|
+
}
|
|
77
|
+
return (ip) => {
|
|
78
|
+
const end = lerIp(ip);
|
|
79
|
+
return Boolean(end) && bloco.check(end.ip, end.familia);
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function ipPermitido(ip, lista) {
|
|
84
|
+
return criarConferidor(lista)(ip);
|
|
85
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "localhostly",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Expose any local port on a public HTTPS URL, behind NAT, without opening ports.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"tunnel",
|
|
@@ -27,10 +27,14 @@
|
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
29
|
"test": "node --test \"testes/*.test.mjs\"",
|
|
30
|
-
"servidor": "node server/index.js"
|
|
30
|
+
"servidor": "node server/index.js",
|
|
31
|
+
"seo:check": "node scripts/seo/check.mjs"
|
|
31
32
|
},
|
|
32
33
|
"license": "MIT",
|
|
33
34
|
"dependencies": {
|
|
34
35
|
"ws": "^8.22.2"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"cheerio": "1.2.0"
|
|
35
39
|
}
|
|
36
40
|
}
|