Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ Um exemplo de `configuracao.delprops` para um projeto em Delégua:
liquido.arquetipo = 'rest'
liquido.linguagem = 'delegua'
liquido.roteador.cors = verdadeiro
liquido.roteador.origensCors = '*'
liquido.roteador.bodyParser = verdadeiro
liquido.roteador.morgan = verdadeiro
liquido.roteador.cookieParser = verdadeiro
Expand All @@ -229,6 +230,7 @@ Para usar Pituguês, basta alterar a propriedade `linguagem`:
liquido.arquetipo = 'rest'
liquido.linguagem = 'pituguês'
liquido.roteador.cors = verdadeiro
liquido.roteador.origensCors = '*'
liquido.roteador.bodyParser = verdadeiro
liquido.roteador.morgan = verdadeiro
liquido.roteador.cookieParser = verdadeiro
Expand All @@ -237,6 +239,8 @@ liquido.roteador.json = verdadeiro
liquido.roteador.helmet = verdadeiro
```

A propriedade `liquido.roteador.origensCors` define a(s) origem(ns) permitida(s) para CORS, separadas por vírgula (por exemplo, `'https://meusite.com.br, https://admin.meusite.com.br'`). O valor `'*'` libera qualquer origem e é adequado apenas para desenvolvimento — em produção, restrinja para o(s) domínio(s) da aplicação. Quando o CORS está habilitado com `'*'`, Liquido exibe um aviso na inicialização.

### Servindo arquivos estáticos

Uma aplicação em Liquido pode servir arquivos estáticos se o roteador tiver uma configuração de diretório correspondente. Por exemplo:
Expand Down
3 changes: 3 additions & 0 deletions configuracao.delprops
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ liquido.roteador.diretorioEstatico = 'publico'

// Configuração do roteador.
liquido.roteador.cors = verdadeiro
// CORS liberado para qualquer origem ('*') é adequado apenas para desenvolvimento.
// Em produção, restrinja para o(s) domínio(s) da aplicação, separados por vírgula.
liquido.roteador.origensCors = '*'
liquido.roteador.bodyParser = verdadeiro
liquido.roteador.morgan = verdadeiro
liquido.roteador.cookieParser = verdadeiro
Expand Down
8 changes: 8 additions & 0 deletions fontes/delprops/roteador.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ const roteador: DefinicaoPropriedade[] = [
tipo: 'logico',
detalhe: 'Habilita CORS (padrão: falso).',
},
{
nome: 'origensCors',
tipo: 'texto',
detalhe:
"Origem(ns) permitida(s) para CORS, separadas por vírgula. " +
"O padrão '*' libera qualquer origem e é adequado apenas para desenvolvimento; " +
"em produção, restrinja para o(s) domínio(s) da aplicação.",
},
{
nome: 'bodyParser',
tipo: 'logico',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ConfiguracaoComum } from "./configuracao-comum";
export class ConfiguracaoRoteador extends ConfiguracaoComum {
diretorioEstatico: string = 'publico';
cors: boolean = false;
origensCors: string = '*';
bodyParser: boolean = true;
morgan: boolean = false;
cookieParser: boolean = true;
Expand All @@ -20,6 +21,7 @@ export class ConfiguracaoRoteador extends ConfiguracaoComum {
const roteador = componentes['roteador'] as RoteadorInterface;
roteador.ativarDesativarBodyParser(this.bodyParser);
roteador.ativarDesativarCors(this.cors);
roteador.configurarOrigensCors(this.origensCors);
roteador.ativarDesativarCookieParser(this.cookieParser);
roteador.ativarDesativarExpressJson(this.json);
roteador.ativarDesativarHelmet(this.helmet);
Expand Down
45 changes: 44 additions & 1 deletion fontes/infraestrutura/roteador/roteador.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export class Roteador implements RoteadorInterface {
bodyParser = false;

cors = false;
origensCors = '*';
passport = false;

constructor(autoDocumentador: AutoDocumentador) {
Expand Down Expand Up @@ -122,7 +123,17 @@ export class Roteador implements RoteadorInterface {
}

if (this.cors) {
this.aplicacao.use(cors());
const opcoesCors = this.resolverOpcoesCors();
if (opcoesCors === undefined) {
console.log(
"[Liquido] CORS habilitado para qualquer origem ('*'). " +
"Adequado apenas para desenvolvimento; em produção, restrinja com " +
"liquido.roteador.origensCors = 'https://seudominio.com.br' em configuracao.delprops."
);
this.aplicacao.use(cors());
} else {
this.aplicacao.use(cors(opcoesCors));
}
}

if (this.passport) {
Expand All @@ -138,6 +149,38 @@ export class Roteador implements RoteadorInterface {
this.cors = valor;
}

/**
* Define a(s) origem(ns) permitida(s) para CORS. Aceita uma única origem
* ou várias separadas por vírgula. O valor '*' (padrão) libera qualquer
* origem e deve ser usado apenas em desenvolvimento.
*/
configurarOrigensCors(origem: string): void {
this.origensCors = origem && origem.trim().length > 0 ? origem : '*';
}

/**
* Traduz `origensCors` para as opções do middleware `cors`.
* @returns `undefined` quando a origem é '*' (comportamento padrão do
* middleware, que libera qualquer origem), ou um objeto com a
* lista de origens permitidas.
*/
resolverOpcoesCors(): { origin: string[] } | undefined {
if (this.origensCors === '*') {
return undefined;
}

const origens = this.origensCors
.split(',')
.map(origem => origem.trim())
.filter(origem => origem.length > 0);

if (origens.length === 0) {
return undefined;
}

return { origin: origens };
}

ativarDesativarPassport(valor: boolean): void {
this.passport = valor;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ liquido.roteador.diretorioEstatico = 'publico'

// Configuração do roteador.
liquido.roteador.cors = verdadeiro
// CORS liberado para qualquer origem ('*') é adequado apenas para desenvolvimento.
// Em produção, restrinja para o(s) domínio(s) da aplicação, separados por vírgula.
liquido.roteador.origensCors = '*'
liquido.roteador.bodyParser = verdadeiro
liquido.roteador.morgan = verdadeiro
liquido.roteador.cookieParser = verdadeiro
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ liquido.roteador.diretorioEstatico = 'publico'

// Configuração do roteador.
liquido.roteador.cors = verdadeiro
// CORS liberado para qualquer origem ('*') é adequado apenas para desenvolvimento.
// Em produção, restrinja para o(s) domínio(s) da aplicação, separados por vírgula.
liquido.roteador.origensCors = '*'
liquido.roteador.bodyParser = verdadeiro
liquido.roteador.morgan = verdadeiro
liquido.roteador.cookieParser = verdadeiro
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ liquido.roteador.diretorioEstatico = 'publico'

// Configuração do roteador.
liquido.roteador.cors = verdadeiro
// CORS liberado para qualquer origem ('*') é adequado apenas para desenvolvimento.
// Em produção, restrinja para o(s) domínio(s) da aplicação, separados por vírgula.
liquido.roteador.origensCors = '*'
liquido.roteador.bodyParser = verdadeiro
liquido.roteador.morgan = verdadeiro
liquido.roteador.cookieParser = verdadeiro
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ liquido.roteador.diretorioEstatico = 'publico'

// Configuração do roteador.
liquido.roteador.cors = verdadeiro
// CORS liberado para qualquer origem ('*') é adequado apenas para desenvolvimento.
// Em produção, restrinja para o(s) domínio(s) da aplicação, separados por vírgula.
liquido.roteador.origensCors = '*'
liquido.roteador.bodyParser = verdadeiro
liquido.roteador.morgan = verdadeiro
liquido.roteador.cookieParser = verdadeiro
Expand Down
1 change: 1 addition & 0 deletions fontes/interfaces/roteador-interface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export interface RoteadorInterface {
ativarDesativarCors(valor: boolean): void;
configurarOrigensCors(origem: string): void;
ativarDesativarPassport(valor: boolean): void;
ativarDesativarCookieParser(valor: boolean): void;
ativarDesativarExpressJson(valor: boolean): void;
Expand Down
4 changes: 4 additions & 0 deletions testes/infraestrutura/centro-configuracoes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ describe('Testes das classes de configuração', () => {
const roteador = {
ativarDesativarBodyParser: jest.fn(),
ativarDesativarCors: jest.fn(),
configurarOrigensCors: jest.fn(),
ativarDesativarCookieParser: jest.fn(),
ativarDesativarExpressJson: jest.fn(),
ativarDesativarHelmet: jest.fn(),
Expand All @@ -184,6 +185,7 @@ describe('Testes das classes de configuração', () => {
config.configurar({ roteador });
expect(roteador.ativarDesativarBodyParser).toHaveBeenCalledWith(true);
expect(roteador.ativarDesativarCors).toHaveBeenCalledWith(false);
expect(roteador.configurarOrigensCors).toHaveBeenCalledWith('*');
expect(roteador.ativarDesativarCookieParser).toHaveBeenCalledWith(true);
expect(roteador.ativarDesativarExpressJson).toHaveBeenCalledWith(true);
expect(roteador.ativarDesativarHelmet).toHaveBeenCalledWith(true);
Expand All @@ -198,6 +200,7 @@ describe('Testes das classes de configuração', () => {
const roteador = {
ativarDesativarBodyParser: jest.fn(),
ativarDesativarCors: jest.fn(),
configurarOrigensCors: jest.fn(),
ativarDesativarCookieParser: jest.fn(),
ativarDesativarExpressJson: jest.fn(),
ativarDesativarHelmet: jest.fn(),
Expand Down Expand Up @@ -282,6 +285,7 @@ describe('Testes das classes de configuração', () => {
const roteador = {
ativarDesativarBodyParser: jest.fn(),
ativarDesativarCors: jest.fn(),
configurarOrigensCors: jest.fn(),
ativarDesativarCookieParser: jest.fn(),
ativarDesativarExpressJson: jest.fn(),
ativarDesativarHelmet: jest.fn(),
Expand Down
139 changes: 139 additions & 0 deletions testes/infraestrutura/cors-origem.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { Roteador } from '../../fontes/infraestrutura/roteador/roteador';
import { ConfiguracaoRoteador } from '../../fontes/infraestrutura/centro-configuracoes/configuracao-roteador';
import { AutoDocumentador } from '../../fontes/infraestrutura/auto-documentacao/auto-documentador';

/**
* Testes de regressão para o achado B4 (issue #119):
* a origem do CORS deve ser explícita e configurável por
* `liquido.roteador.origensCors`, com aviso quando liberada para
* qualquer origem ('*').
*/
describe('CORS com origem configurável', () => {
const criarRoteador = (): Roteador => new Roteador(new AutoDocumentador());

describe('Roteador.resolverOpcoesCors', () => {
it("padrão '*' resolve para undefined (comportamento liberado do middleware)", () => {
const roteador = criarRoteador();
expect(roteador.resolverOpcoesCors()).toBeUndefined();
});

it('uma origem única resolve para lista com um item', () => {
const roteador = criarRoteador();
roteador.configurarOrigensCors('https://meusite.com.br');
expect(roteador.resolverOpcoesCors()).toEqual({
origin: ['https://meusite.com.br']
});
});

it('múltiplas origens separadas por vírgula resolvem para lista, ignorando espaços', () => {
const roteador = criarRoteador();
roteador.configurarOrigensCors('https://a.com.br, https://b.com.br ,https://c.com.br');
expect(roteador.resolverOpcoesCors()).toEqual({
origin: ['https://a.com.br', 'https://b.com.br', 'https://c.com.br']
});
});

it('valor vazio ou apenas vírgulas volta ao comportamento padrão', () => {
const roteador = criarRoteador();
roteador.configurarOrigensCors('');
expect(roteador.resolverOpcoesCors()).toBeUndefined();

roteador.configurarOrigensCors(' , ,');
expect(roteador.resolverOpcoesCors()).toBeUndefined();
});
});

describe('Aviso de desenvolvimento na inicialização', () => {
let espiaoConsole: jest.SpyInstance;

beforeEach(() => {
espiaoConsole = jest.spyOn(console, 'log').mockImplementation(() => {});
});

afterEach(() => {
espiaoConsole.mockRestore();
});

const saidaConsole = (): string =>
espiaoConsole.mock.calls.map((chamada) => String(chamada[0])).join('\n');

it("deve avisar quando CORS está habilitado com origem '*'", () => {
const roteador = criarRoteador();
roteador.ativarDesativarCors(true);
roteador.iniciarMiddlewares();

expect(saidaConsole()).toContain("CORS habilitado para qualquer origem ('*')");
expect(saidaConsole()).toContain('apenas para desenvolvimento');
});

it('não deve avisar quando a origem está restringida', () => {
const roteador = criarRoteador();
roteador.ativarDesativarCors(true);
roteador.configurarOrigensCors('https://meusite.com.br');
roteador.iniciarMiddlewares();

expect(saidaConsole()).not.toContain('CORS habilitado para qualquer origem');
});

it('não deve avisar quando CORS está desabilitado', () => {
const roteador = criarRoteador();
roteador.iniciarMiddlewares();

expect(saidaConsole()).not.toContain('CORS habilitado para qualquer origem');
});
});

describe('ConfiguracaoRoteador', () => {
it("deve ter '*' como padrão de origensCors", () => {
const configuracao = new ConfiguracaoRoteador();
expect(configuracao.origensCors).toBe('*');
});

it('deve repassar origensCors ao roteador em configurar()', () => {
const configuracao = new ConfiguracaoRoteador({
cors: true,
origensCors: 'https://meusite.com.br'
});

const roteadorSimulado = {
ativarDesativarBodyParser: jest.fn(),
ativarDesativarCors: jest.fn(),
configurarOrigensCors: jest.fn(),
ativarDesativarCookieParser: jest.fn(),
ativarDesativarExpressJson: jest.fn(),
ativarDesativarHelmet: jest.fn(),
ativarDesativarMorgan: jest.fn(),
ativarDesativarPassport: jest.fn()
};

configuracao.configurar({ roteador: roteadorSimulado });

expect(roteadorSimulado.ativarDesativarCors).toHaveBeenCalledWith(true);
expect(roteadorSimulado.configurarOrigensCors).toHaveBeenCalledWith('https://meusite.com.br');
});
});

describe('Templates do scaffold', () => {
it.each([
['delegua/api-rest'],
['delegua/mvc'],
['pitugues/api-rest'],
['pitugues/mvc']
])('template %s deve declarar origensCors explicitamente com aviso', (template: string) => {
const sistemaArquivos = require('fs');
const caminho = require('path');
const conteudo = sistemaArquivos.readFileSync(
caminho.join(
__dirname,
'../../fontes/interface-linha-comando/exemplos',
template,
'configuracao.delprops'
),
'utf-8'
);

expect(conteudo).toContain("liquido.roteador.origensCors = '*'");
expect(conteudo).toContain('apenas para desenvolvimento');
});
});
});
Loading