@hitchy/plugin-odem-rest 0.4.8 → 0.5.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2018 cepharum GmbH
3
+ Copyright (c) 2022 cepharum GmbH
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -29,8 +29,30 @@
29
29
  "use strict";
30
30
 
31
31
  module.exports = function() {
32
+ const Config = this.config;
32
33
  const Services = this.runtime.services;
33
34
 
35
+ const CommonlyAcceptedHeaders = [
36
+ "Accept", "Accept-Language", "Authorization", "Content-Language",
37
+ "Content-Type", "If-Match", "If-None-Match", "If-Modified-Since",
38
+ "If-Unmodified-Since", "If-Range", "DNT", "Expect"
39
+ ];
40
+
41
+ const Accepted = {
42
+ schema: {
43
+ methods: ["GET"],
44
+ headers: CommonlyAcceptedHeaders,
45
+ },
46
+ model: {
47
+ methods: [ "GET", "POST", "HEAD" ],
48
+ headers: CommonlyAcceptedHeaders,
49
+ },
50
+ item: {
51
+ methods: [ "GET", "PUT", "PATCH", "HEAD", "DELETE" ],
52
+ headers: CommonlyAcceptedHeaders,
53
+ },
54
+ };
55
+
34
56
  /**
35
57
  * Handles CORS-related behaviours.
36
58
  *
@@ -44,8 +66,38 @@ module.exports = function() {
44
66
  * @returns {HitchyRequestPolicyHandler} generated function suitable for registering as routing policy handler
45
67
  */
46
68
  static getCommonRequestFilter() {
47
- return ( _, res, next ) => {
48
- res.setHeader( "Access-Control-Allow-Origin", "*" );
69
+ return ( req, res, next ) => {
70
+ const origin = req.headers.origin;
71
+
72
+ if ( origin != null ) {
73
+ const { model: { origins } = {} } = Config;
74
+
75
+ if ( !origins || ( Array.isArray( origins ) && origins.indexOf( origin ) > -1 ) ) {
76
+ res.setHeader( "Access-Control-Allow-Origin", origin );
77
+ res.setHeader( "Access-Control-Max-Age", 600 );
78
+ }
79
+ }
80
+
81
+ next();
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Generates function for use as a routing policy filtering CORS-related
87
+ * aspects of requests for all models' schemata.
88
+ *
89
+ * @returns {HitchyRequestPolicyHandler} generated function suitable for registering as routing policy handler
90
+ */
91
+ static getRequestFilterForSchemata() {
92
+ return ( req, res, next ) => {
93
+ if ( !res.headersSent ) {
94
+ this.handleMethods( null, null, req, res, Accepted.schema.methods );
95
+
96
+ if ( res.hasHeader( "Access-Control-Allow-Origin" ) ) {
97
+ this.handleHeaders( null, null, req, res, Accepted.schema.headers );
98
+ }
99
+ }
100
+
49
101
  next();
50
102
  };
51
103
  }
@@ -59,8 +111,75 @@ module.exports = function() {
59
111
  */
60
112
  static getRequestFilterForModel( model ) { // eslint-disable-line no-unused-vars
61
113
  return ( req, res, next ) => {
62
- if ( Services.OdemRestSchema.mayBeExposed( req, model ) ) {
63
- res.setHeader( "Access-Control-Allow-Origin", "*" );
114
+ if ( !res.headersSent ) {
115
+ if ( Services.OdemRestSchema.mayBeExposed( req, model ) ) {
116
+ this.handleMethods( model, null, req, res, Accepted.model.methods );
117
+
118
+ if ( res.hasHeader( "Access-Control-Allow-Origin" ) ) {
119
+ this.handleHeaders( model, null, req, res, Accepted.model.headers );
120
+ }
121
+ } else {
122
+ // revoke any access permission commonly granted before
123
+ res.removeHeader( "Access-Control-Allow-Origin" );
124
+ res.removeHeader( "Access-Control-Allow-Methods" );
125
+ res.removeHeader( "Access-Control-Allow-Headers" );
126
+ }
127
+ }
128
+
129
+ next();
130
+ };
131
+ }
132
+
133
+ /**
134
+ * Generates function for use as a routing policy filtering CORS-related
135
+ * aspects of requests in scope of provided model.
136
+ *
137
+ * @param {class<Model>} model class of particular model
138
+ * @returns {HitchyRequestPolicyHandler} generated function suitable for registering as routing policy handler
139
+ */
140
+ static getRequestFilterForModelSchema( model ) { // eslint-disable-line no-unused-vars
141
+ return ( req, res, next ) => {
142
+ if ( !res.headersSent ) {
143
+ if ( Services.OdemRestSchema.mayBeExposed( req, model ) ) {
144
+ this.handleMethods( model, null, req, res, Accepted.schema.methods );
145
+
146
+ if ( res.hasHeader( "Access-Control-Allow-Origin" ) ) {
147
+ this.handleHeaders( model, null, req, res, Accepted.schema.headers );
148
+ }
149
+ } else {
150
+ // revoke any access permission commonly granted before
151
+ res.removeHeader( "Access-Control-Allow-Origin" );
152
+ res.removeHeader( "Access-Control-Allow-Methods" );
153
+ res.removeHeader( "Access-Control-Allow-Headers" );
154
+ }
155
+ }
156
+
157
+ next();
158
+ };
159
+ }
160
+
161
+ /**
162
+ * Generates function for use as a routing policy filtering CORS-related
163
+ * aspects of requests in scope of provided model.
164
+ *
165
+ * @param {class<Model>} model class of particular model
166
+ * @returns {HitchyRequestPolicyHandler} generated function suitable for registering as routing policy handler
167
+ */
168
+ static getRequestFilterForModelItem( model ) { // eslint-disable-line no-unused-vars
169
+ return ( req, res, next ) => {
170
+ if ( !res.headersSent && req.params.uuid !== ".schema" ) {
171
+ if ( Services.OdemRestSchema.mayBeExposed( req, model ) ) {
172
+ this.handleMethods( model, req.params.uuid, req, res, Accepted.item.methods );
173
+
174
+ if ( res.hasHeader( "Access-Control-Allow-Origin" ) ) {
175
+ this.handleHeaders( model, req.params.uuid, req, res, Accepted.item.headers );
176
+ }
177
+ } else {
178
+ // revoke any access permission commonly granted before
179
+ res.removeHeader( "Access-Control-Allow-Origin" );
180
+ res.removeHeader( "Access-Control-Allow-Methods" );
181
+ res.removeHeader( "Access-Control-Allow-Headers" );
182
+ }
64
183
  }
65
184
 
66
185
  next();
@@ -81,6 +200,87 @@ module.exports = function() {
81
200
  next();
82
201
  };
83
202
  }
203
+
204
+ /**
205
+ * Injects response header describing available request methods for URL
206
+ * processing selected model and optionally addressed item.
207
+ *
208
+ * @param {class<Model>} model implementation of model selected by request URL
209
+ * @param {string} item UUID of model's item addressed in request URL
210
+ * @param {HitchyIncomingMessage} req request descriptor
211
+ * @param {HitchyServerResponse} res response manager
212
+ * @param {string[]} accepted comma-separated list of methods to accept by default
213
+ * @returns {void}
214
+ * @protected
215
+ */
216
+ static handleMethods( model, item, req, res, accepted ) {
217
+ const methods = req.headers["access-control-request-method"] || "";
218
+
219
+ if ( methods.length > 0 && res.hasHeader( "Access-Control-Allow-Origin" ) ) {
220
+ // CORS preflight request
221
+ let filtered = methods
222
+ .trim()
223
+ .split( /[\s,]+/ )
224
+ .filter( method => method && accepted.indexOf( method ) > -1 );
225
+
226
+ if ( !filtered.length ) {
227
+ filtered = accepted;
228
+ }
229
+
230
+ filtered.sort( ( l, r ) => l.toUpperCase().localeCompare( r.toUpperCase() ) );
231
+
232
+ res.setHeader( "Access-Control-Allow-Methods", filtered.join( "," ) );
233
+ } else if ( req.headers.origin == null ) {
234
+ // legacy OPTIONS request for list of supported methods
235
+ accepted.sort( ( l, r ) => l.toUpperCase().localeCompare( r.toUpperCase() ) );
236
+
237
+ res.setHeader( "Allow", accepted.join( "," ) );
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Injects response header describing available request headers for URL
243
+ * processing selected model and optionally addressed item.
244
+ *
245
+ * @param {class<Model>} model implementation of model selected by request URL
246
+ * @param {string} item UUID of model's item addressed in request URL
247
+ * @param {HitchyIncomingMessage} req request descriptor
248
+ * @param {HitchyServerResponse} res response manager
249
+ * @param {string[]} accepted comma-separated list of accepted headers
250
+ * @returns {void}
251
+ * @protected
252
+ */
253
+ static handleHeaders( model, item, req, res, accepted ) {
254
+ const headers = req.headers["access-control-request-headers"] || "";
255
+
256
+ if ( headers.length > 0 ) {
257
+ // CORS preflight request
258
+ const _accepted = accepted.map( header => header.toLowerCase() );
259
+
260
+ let filtered = headers
261
+ .trim()
262
+ .split( /[\s,]+/ )
263
+ .filter( method => {
264
+ const name = ( method || "" ).toLowerCase();
265
+
266
+ if ( name && _accepted.indexOf( name ) > -1 ) {
267
+ return true;
268
+ }
269
+
270
+ // TODO add support for configuration listing permitted headers
271
+
272
+ return name.startsWith( "x-" );
273
+ } );
274
+
275
+ if ( !filtered.length ) {
276
+ filtered = accepted;
277
+ }
278
+
279
+ filtered.sort( ( l, r ) => l.toLowerCase().localeCompare( r.toLowerCase() ) );
280
+
281
+ res.setHeader( "Access-Control-Allow-Headers", filtered.join( "," ) );
282
+ }
283
+ }
84
284
  }
85
285
 
86
286
  return OdemRestCors;