Skip to content

Commit 6c8d950

Browse files
committed
feat: adding CORS configuration
Signed-off-by: Pawel Psztyc <jarrodek@gmail.com>
1 parent c6bb071 commit 6c8d950

File tree

5 files changed

+220
-1
lines changed

5 files changed

+220
-1
lines changed

package-lock.json

Lines changed: 39 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
},
2424
"dependencies": {
2525
"@advanced-rest-client/events": "^0.2.29",
26+
"@koa/cors": "^3.1.0",
2627
"@koa/router": "^10.1.1",
2728
"amf-client-js": "^5.0.0-SEMANTIC-JSONSCHEMA.6",
2829
"fs-extra": "^10.0.0",
@@ -40,6 +41,7 @@
4041
"@types/chai": "^4.2.11",
4142
"@types/fs-extra": "^9.0.13",
4243
"@types/koa": "^2.13.4",
44+
"@types/koa__cors": "^3.0.3",
4345
"@types/koa__router": "^8.0.9",
4446
"@types/mocha": "^9.0.0",
4547
"@types/unzipper": "^0.10.4",

src/Server.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import Koa from 'koa';
22
import http from 'http';
33
import https from 'https';
4+
import cors from '@koa/cors';
45
import { ApiRoutes } from './ApiRoutes.js';
56

7+
/** @typedef {import('@koa/cors').Options} CorsOptions */
68
/** @typedef {import('../types').RunningServer} RunningServer */
79
/** @typedef {import('../types').SupportedServer} SupportedServer */
810
/** @typedef {import('../types').ServerConfiguration} ServerConfiguration */
@@ -39,7 +41,12 @@ export class Server {
3941
* @param {string=} prefix The prefix to use with the API routes. E.g. /api/v1
4042
*/
4143
setupRoutes(prefix) {
42-
const handler = new ApiRoutes(this.opts);
44+
const { opts } = this;
45+
if (opts.cors && opts.cors.enabled) {
46+
const config = opts.cors.cors || this.defaultCorsConfig();
47+
this.app.use(cors(config));
48+
}
49+
const handler = new ApiRoutes(opts);
4350
const apiRouter = handler.setup(prefix);
4451
this.app.use(apiRouter.routes());
4552
this.app.use(apiRouter.allowedMethods());
@@ -137,4 +144,13 @@ export class Server {
137144
});
138145
});
139146
}
147+
148+
/**
149+
* @returns {CorsOptions}
150+
*/
151+
defaultCorsConfig() {
152+
return {
153+
allowMethods: 'GET,PUT,POST,DELETE',
154+
};
155+
}
140156
}

test/Cors.test.js

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { assert } from 'chai';
2+
import getPort from 'get-port';
3+
import http from 'http';
4+
import fs from 'fs-extra';
5+
import Server from '../index.js';
6+
import { untilResponse, untilParseResult } from './RequestUtils.js';
7+
8+
describe('CORS settings', () => {
9+
/** @type Server */
10+
let instance;
11+
/** @type number */
12+
let port;
13+
let simpleRamlApi;
14+
before(async () => {
15+
port = await getPort();
16+
simpleRamlApi = await fs.readFile('test/apis/simple.raml', 'utf8');
17+
});
18+
19+
describe('CORS enabled with defaults', () => {
20+
before(async () => {
21+
port = await getPort();
22+
instance = new Server({
23+
cors: {
24+
enabled: true,
25+
}
26+
});
27+
instance.setupRoutes();
28+
await instance.startHttp(port);
29+
simpleRamlApi = await fs.readFile('test/apis/simple.raml', 'utf8');
30+
});
31+
32+
after(async () => {
33+
await instance.cleanup();
34+
await instance.stopHttp();
35+
});
36+
37+
it('has the CORS headers for OPTIONS request', async () => {
38+
const request = http.request({
39+
hostname: 'localhost',
40+
port,
41+
path: '/text',
42+
method: 'OPTIONS',
43+
headers: {
44+
'Content-Type': 'application/raml',
45+
'Authorization': 'Basic test',
46+
'x-api-vendor': 'RAML 1.0',
47+
'origin': 'https://www.api.com',
48+
'Accept-Encoding': 'gzip,deflate',
49+
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0',
50+
'Accept': 'application/json,application/ld+json',
51+
'Connection': 'close',
52+
'Access-Control-Request-Method': 'POST',
53+
'Access-Control-Request-Headers': 'x-api-vendor, Content-Type',
54+
},
55+
});
56+
const result = await untilResponse(request);
57+
const { statusCode, headers } = result;
58+
assert.equal(statusCode, 204, 'has the 204 status code');
59+
60+
assert.equal(headers['access-control-allow-origin'], 'https://www.api.com', 'has access-control-allow-origin');
61+
assert.equal(headers['access-control-allow-methods'], 'GET,PUT,POST,DELETE', 'has access-control-allow-methods');
62+
assert.equal(headers['access-control-allow-headers'], 'x-api-vendor, Content-Type', 'has access-control-allow-headers');
63+
});
64+
65+
it('has the CORS headers for POST request', async () => {
66+
const request = http.request({
67+
hostname: 'localhost',
68+
port,
69+
path: '/text',
70+
method: 'POST',
71+
headers: {
72+
'Content-Type': 'application/raml',
73+
'Authorization': 'Basic test',
74+
'x-api-vendor': 'RAML 1.0',
75+
'origin': 'https://www.api.com',
76+
'Accept-Encoding': 'gzip,deflate',
77+
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0',
78+
'Accept': 'application/json,application/ld+json',
79+
'Connection': 'close',
80+
'Access-Control-Request-Method': 'POST',
81+
'Access-Control-Request-Headers': 'x-api-vendor, Content-Type',
82+
},
83+
});
84+
request.write(simpleRamlApi);
85+
const result = await untilResponse(request);
86+
const { headers } = result;
87+
88+
assert.equal(headers['access-control-allow-origin'], 'https://www.api.com', 'has access-control-allow-origin');
89+
});
90+
});
91+
92+
describe('CORS enabled with passed configuration', () => {
93+
before(async () => {
94+
port = await getPort();
95+
instance = new Server({
96+
cors: {
97+
enabled: true,
98+
cors: {
99+
origin: 'https://my.domain.org'
100+
},
101+
}
102+
});
103+
instance.setupRoutes();
104+
await instance.startHttp(port);
105+
simpleRamlApi = await fs.readFile('test/apis/simple.raml', 'utf8');
106+
});
107+
108+
after(async () => {
109+
await instance.cleanup();
110+
await instance.stopHttp();
111+
});
112+
113+
it('has the CORS headers for OPTIONS request', async () => {
114+
const request = http.request({
115+
hostname: 'localhost',
116+
port,
117+
path: '/text',
118+
method: 'OPTIONS',
119+
headers: {
120+
'Content-Type': 'application/raml',
121+
'Authorization': 'Basic test',
122+
'x-api-vendor': 'RAML 1.0',
123+
'origin': 'https://www.api.com',
124+
'Accept-Encoding': 'gzip,deflate',
125+
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0',
126+
'Accept': 'application/json,application/ld+json',
127+
'Connection': 'close',
128+
'Access-Control-Request-Method': 'POST',
129+
'Access-Control-Request-Headers': 'x-api-vendor, Content-Type',
130+
},
131+
});
132+
const result = await untilResponse(request);
133+
const { statusCode, headers } = result;
134+
assert.equal(statusCode, 204, 'has the 204 status code');
135+
136+
assert.equal(headers['access-control-allow-origin'], 'https://my.domain.org', 'has access-control-allow-origin');
137+
assert.equal(headers['access-control-allow-methods'], 'GET,HEAD,PUT,POST,DELETE,PATCH', 'has access-control-allow-methods');
138+
assert.equal(headers['access-control-allow-headers'], 'x-api-vendor, Content-Type', 'has access-control-allow-headers');
139+
});
140+
});
141+
});

types.d.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import http from 'http';
22
import https from 'https';
3+
import { Options as CorsOptions } from '@koa/cors';
34
import { AmfParser } from './src/AmfParser';
45

56
export interface AmfProcessItem {
@@ -72,6 +73,10 @@ export interface RunningServer {
7273

7374
export interface ServerConfiguration {
7475
parser?: ParserConfiguration;
76+
/**
77+
* CORS configuration.
78+
*/
79+
cors?: CorsConfiguration;
7580
}
7681

7782
export interface ParserConfiguration {
@@ -96,3 +101,19 @@ export interface ApiParsingResult {
96101
*/
97102
vendor: ParserVendors;
98103
}
104+
105+
export interface CorsConfiguration {
106+
/**
107+
* When set it enables CORS headers for the API.
108+
* By default it is disabled.
109+
*/
110+
enabled?: boolean;
111+
/**
112+
* Optional configuration passed to `@koa/cors`.
113+
* See more here: https://github.com/koajs/cors
114+
* When not set it uses default values.
115+
*
116+
* Note, default values apply the request's origin to the `Access-Control-Allow-Origin` header.
117+
*/
118+
cors?: CorsOptions;
119+
}

0 commit comments

Comments
 (0)