@ti-engine/web-framework 1.15.0 → 1.17.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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
This document will contain the list of changes made to the framework. The format is based on the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
|
|
4
4
|
|
|
5
|
+
## Version 1.17.0
|
|
6
|
+
|
|
7
|
+
Route-registration seams so an application subclass can add its own Express routes and unprotected-route patterns — enabling public, content-driven sites (the first consumer being the standalone author's site) to layer a catch-all content resolver over the framework without reaching into private state.
|
|
8
|
+
|
|
9
|
+
* feat(web-server): add `TiWebServer.registerRoute( method, path, ...handlers )` — registers a custom route on the underlying Express app from a `defineWebApplicationRoutes()` override (after `super()`), so a catch-all resolver can be mounted after the framework's own routes but before its `*splat` 404 handler. Limited to route-scoped verbs (get/post/put/patch/delete/options/head/all); raises `E_GEN_INVALID_ARGUMENT_TYPE` for any other method and `E_GEN_NOT_INITIALIZED` if called before the Express app exists
|
|
10
|
+
* feat(web-server): add `TiWebServer.addUnprotectedRoute( pattern )` — appends a string (exact-match) or RegExp (tested) pattern to the unprotected-routes list from a `defineUnprotectedRoutes()` override, so a public-by-default site can invert the framework's protect-by-default stance; non-string/non-RegExp values are ignored with a warning
|
|
11
|
+
* refactor(web-server): extract the unprotected-route matching loop from `isUnprotectedRoute()` into the pure, unit-tested `isRouteInList()` helper (behavior unchanged, including the defensive `lastIndex` reset), and add the `normalizeRegistrableMethod()` helper — both exported for testing alongside the existing `RE_*` matcher constants
|
|
12
|
+
* build(release): bump package version from `1.16.0` to `1.17.0`
|
|
13
|
+
|
|
14
|
+
## Version 1.16.0
|
|
15
|
+
|
|
16
|
+
Support explicitly trusted request origins so state-changing requests (e.g. login) work behind proxies that do not present the app's external host — most notably GitHub Codespaces port forwarding (CA-90).
|
|
17
|
+
|
|
18
|
+
* feat(web-framework): add `TI_WEB_TRUSTED_ORIGINS` (comma-separated) / `config.trustedOrigins`. The `originRefererValidationHandler` now accepts a non-GET request whose `Origin`/`Referer` matches the server-reconstructed base URL **or** any configured trusted origin. Previously such a request behind a proxy that rewrote/omitted the forwarded host was rejected with `E_WEB_INVALID_REQUEST_PARAMETERS` (HTTP 403). Backward compatible (empty list = prior behavior); the CSRF double-submit token check is unchanged and still enforced
|
|
19
|
+
* build(release): bump package version from `1.15.0` to `1.16.0`
|
|
20
|
+
|
|
5
21
|
## Version 1.15.0
|
|
6
22
|
|
|
7
23
|
A dedicated health endpoint, a `TI_WEB_AUTH_METHODS` env override, and login-page gating for every auth method — completing the container-friendly auth/health story for the competence deployment (CA-90).
|
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
<div x-id="['component-flyout']"
|
|
2
|
-
x-data="tiComponentSidebarFlyout('{ti-config-key}')"
|
|
3
|
-
class="ti-sidebar-flyout-container">
|
|
4
|
-
<!--
|
|
5
|
-
Trigger slot. The placeholder consumer can replace this with any custom button — typically a user avatar
|
|
6
|
-
or app menu trigger. The trigger MUST set x-ref="flyoutButton" and call toggle() to integrate with the
|
|
7
|
-
encapsulated flyout state. If the consumer omits inner content, this default icon button is used.
|
|
8
|
-
-->
|
|
9
|
-
<ti-slot>
|
|
10
|
-
<button x-on:click.stop="toggle()"
|
|
11
|
-
x-on:ti-close-all-flyout="close()"
|
|
12
|
-
x-bind:aria-expanded="isOpen"
|
|
13
|
-
x-bind:aria-controls="$id('component-flyout')"
|
|
14
|
-
x-bind:aria-label="menuTitle"
|
|
15
|
-
x-ref="flyoutButton"
|
|
16
|
-
type="button" class="ti-sidebar-button">
|
|
17
|
-
<span class="ti-icon" x-bind:class="icon" aria-hidden="true"></span>
|
|
18
|
-
</button>
|
|
19
|
-
</ti-slot>
|
|
20
|
-
<div x-on:click.outside="close()"
|
|
21
|
-
x-bind:id="$id('component-flyout')"
|
|
22
|
-
x-show="isOpen"
|
|
23
|
-
x-ref="flyoutPanel"
|
|
24
|
-
role="menu" class="ti-sidebar-flyout">
|
|
25
|
-
<template x-for="(buttonConfig, index) in buttonConfigs">
|
|
26
|
-
<button x-on:click="close()"
|
|
27
|
-
x-bind:hx-get="(!buttonConfig.action.method || buttonConfig.action.method === 'get') ? buttonConfig.action.href : null"
|
|
28
|
-
x-bind:hx-post="(buttonConfig.action.method && buttonConfig.action.method === 'post') ? buttonConfig.action.href : null"
|
|
29
|
-
x-bind:hx-target="buttonConfig.action.target"
|
|
30
|
-
x-bind:hx-swap="buttonConfig.action.swap"
|
|
31
|
-
hx-push-url="true"
|
|
32
|
-
type="button" class="ti-sidebar-flyout-item">
|
|
33
|
-
<span class="ti-icon" x-bind:class="buttonConfig.icon" aria-hidden="true"></span><span x-text="buttonConfig.title"></span>
|
|
34
|
-
</button>
|
|
35
|
-
</template>
|
|
36
|
-
</div>
|
|
1
|
+
<div x-id="['component-flyout']"
|
|
2
|
+
x-data="tiComponentSidebarFlyout('{ti-config-key}')"
|
|
3
|
+
class="ti-sidebar-flyout-container">
|
|
4
|
+
<!--
|
|
5
|
+
Trigger slot. The placeholder consumer can replace this with any custom button — typically a user avatar
|
|
6
|
+
or app menu trigger. The trigger MUST set x-ref="flyoutButton" and call toggle() to integrate with the
|
|
7
|
+
encapsulated flyout state. If the consumer omits inner content, this default icon button is used.
|
|
8
|
+
-->
|
|
9
|
+
<ti-slot>
|
|
10
|
+
<button x-on:click.stop="toggle()"
|
|
11
|
+
x-on:ti-close-all-flyout="close()"
|
|
12
|
+
x-bind:aria-expanded="isOpen"
|
|
13
|
+
x-bind:aria-controls="$id('component-flyout')"
|
|
14
|
+
x-bind:aria-label="menuTitle"
|
|
15
|
+
x-ref="flyoutButton"
|
|
16
|
+
type="button" class="ti-sidebar-button">
|
|
17
|
+
<span class="ti-icon" x-bind:class="icon" aria-hidden="true"></span>
|
|
18
|
+
</button>
|
|
19
|
+
</ti-slot>
|
|
20
|
+
<div x-on:click.outside="close()"
|
|
21
|
+
x-bind:id="$id('component-flyout')"
|
|
22
|
+
x-show="isOpen"
|
|
23
|
+
x-ref="flyoutPanel"
|
|
24
|
+
role="menu" class="ti-sidebar-flyout">
|
|
25
|
+
<template x-for="(buttonConfig, index) in buttonConfigs">
|
|
26
|
+
<button x-on:click="close()"
|
|
27
|
+
x-bind:hx-get="(!buttonConfig.action.method || buttonConfig.action.method === 'get') ? buttonConfig.action.href : null"
|
|
28
|
+
x-bind:hx-post="(buttonConfig.action.method && buttonConfig.action.method === 'post') ? buttonConfig.action.href : null"
|
|
29
|
+
x-bind:hx-target="buttonConfig.action.target"
|
|
30
|
+
x-bind:hx-swap="buttonConfig.action.swap"
|
|
31
|
+
hx-push-url="true"
|
|
32
|
+
type="button" class="ti-sidebar-flyout-item">
|
|
33
|
+
<span class="ti-icon" x-bind:class="buttonConfig.icon" aria-hidden="true"></span><span x-text="buttonConfig.title"></span>
|
|
34
|
+
</button>
|
|
35
|
+
</template>
|
|
36
|
+
</div>
|
|
37
37
|
</div>
|
package/bin/web-server.js
CHANGED
|
@@ -301,7 +301,7 @@ class TiWebServer extends ServiceConsumer {
|
|
|
301
301
|
store: new SessionStore()
|
|
302
302
|
} ) );
|
|
303
303
|
this.#webServer.use( webHandlers.csrfInitHandler( this ) );
|
|
304
|
-
this.#webServer.use( webHandlers.originRefererValidationHandler() );
|
|
304
|
+
this.#webServer.use( webHandlers.originRefererValidationHandler( this ) );
|
|
305
305
|
this.#webServer.use( webHandlers.csrfProtectionHandler() );
|
|
306
306
|
|
|
307
307
|
// Set up the web server routes:
|
|
@@ -493,21 +493,7 @@ class TiWebServer extends ServiceConsumer {
|
|
|
493
493
|
*/
|
|
494
494
|
isUnprotectedRoute( route ) {
|
|
495
495
|
const pathOnly = String( route || "" ).split( "?" )[ 0 ];
|
|
496
|
-
|
|
497
|
-
for ( let idx = 0; idx < this.#unprotectedRoutes.length; idx++ ) {
|
|
498
|
-
const pattern = this.#unprotectedRoutes[ idx ];
|
|
499
|
-
if ( _.isRegExp( pattern ) ) {
|
|
500
|
-
// Avoid stateful RegExp behavior when 'g' or 'y' flags are present:
|
|
501
|
-
pattern.lastIndex = 0;
|
|
502
|
-
result = pattern.test( pathOnly );
|
|
503
|
-
} else {
|
|
504
|
-
result = ( pattern === pathOnly );
|
|
505
|
-
}
|
|
506
|
-
if ( result ) {
|
|
507
|
-
break;
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
return result;
|
|
496
|
+
return TiWebServer.isRouteInList( this.#unprotectedRoutes, pathOnly );
|
|
511
497
|
}
|
|
512
498
|
|
|
513
499
|
/**
|
|
@@ -574,6 +560,113 @@ class TiWebServer extends ServiceConsumer {
|
|
|
574
560
|
this.#unprotectedRoutes.push( RE_WELL_KNOWN_UNPROTECTED );
|
|
575
561
|
}
|
|
576
562
|
|
|
563
|
+
/**
|
|
564
|
+
* Registers a custom application route on the underlying Express app.
|
|
565
|
+
* <br/>
|
|
566
|
+
* NOTE: Call this from a {@link TiWebServer#defineWebApplicationRoutes} override AFTER invoking the base method,
|
|
567
|
+
* so the framework's own routes keep priority and any catch-all route you add resolves last (it will still be
|
|
568
|
+
* registered before the framework's own `*splat` 404 handler). It is only valid once the Express app exists —
|
|
569
|
+
* i.e., from within {@link TiWebServer#defineWebApplicationRoutes}, which {@link TiWebServer#onStart} invokes.
|
|
570
|
+
*
|
|
571
|
+
* @method
|
|
572
|
+
* @param {string} method One of the supported routing verbs: get, post, put, patch, delete, options, head, all.
|
|
573
|
+
* @param {string|RegExp} path The route path or pattern.
|
|
574
|
+
* @param {...Function} handlers One or more Express route handlers/middleware.
|
|
575
|
+
* @returns {TiWebServer} This instance, to allow chaining.
|
|
576
|
+
* @public
|
|
577
|
+
*/
|
|
578
|
+
registerRoute( method, path, ...handlers ) {
|
|
579
|
+
const verb = TiWebServer.normalizeRegistrableMethod( method );
|
|
580
|
+
if ( verb === null ) {
|
|
581
|
+
throw exceptions.raise( exceptions.exceptionCode.E_GEN_INVALID_ARGUMENT_TYPE, { method: method } );
|
|
582
|
+
}
|
|
583
|
+
if ( !this.#webServer ) {
|
|
584
|
+
throw exceptions.raise( exceptions.exceptionCode.E_GEN_NOT_INITIALIZED, { detail: "registerRoute() called before the Express app was created; call it from a defineWebApplicationRoutes() override." } );
|
|
585
|
+
}
|
|
586
|
+
this.#webServer[ verb ]( path, ...handlers );
|
|
587
|
+
return this;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Adds a pattern to the unprotected-routes list — routes that bypass the authentication gate. A string is
|
|
592
|
+
* matched exactly against the request path; a RegExp is tested against it. Consulted at request time by
|
|
593
|
+
* {@link TiWebServer#isUnprotectedRoute}.
|
|
594
|
+
* <br/>
|
|
595
|
+
* NOTE: Call this from a {@link TiWebServer#defineUnprotectedRoutes} override AFTER invoking the base method, to
|
|
596
|
+
* extend (rather than replace) the defaults.
|
|
597
|
+
*
|
|
598
|
+
* @method
|
|
599
|
+
* @param {string|RegExp} pattern The exact path (string) or path matcher (RegExp) to treat as unprotected.
|
|
600
|
+
* @returns {TiWebServer} This instance, to allow chaining.
|
|
601
|
+
* @public
|
|
602
|
+
*/
|
|
603
|
+
addUnprotectedRoute( pattern ) {
|
|
604
|
+
if ( _.isString( pattern ) || _.isRegExp( pattern ) ) {
|
|
605
|
+
this.#unprotectedRoutes.push( pattern );
|
|
606
|
+
} else {
|
|
607
|
+
logger.log( `Ignored an invalid unprotected-route pattern of type '${ typeof pattern }'; expected a string or RegExp.`, logger.logSeverity.WARNING );
|
|
608
|
+
}
|
|
609
|
+
return this;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/* Static interface */
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* The Express routing verbs that {@link TiWebServer#registerRoute} will register. Deliberately limited to
|
|
616
|
+
* route-scoped methods — `use` (global middleware mounting) is intentionally excluded; add a dedicated seam if
|
|
617
|
+
* middleware mounting is ever needed.
|
|
618
|
+
*
|
|
619
|
+
* @type {Set<string>}
|
|
620
|
+
* @private
|
|
621
|
+
*/
|
|
622
|
+
static #REGISTRABLE_METHODS = new Set( [ "get", "post", "put", "patch", "delete", "options", "head", "all" ] );
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Normalizes an HTTP method to a lower-case Express routing verb, or returns null if it is not a supported,
|
|
626
|
+
* registrable verb. Pure and static; exposed for unit testing — not part of the customization surface.
|
|
627
|
+
*
|
|
628
|
+
* @method
|
|
629
|
+
* @static
|
|
630
|
+
* @param {string} method
|
|
631
|
+
* @returns {string|null}
|
|
632
|
+
* @public
|
|
633
|
+
*/
|
|
634
|
+
static normalizeRegistrableMethod( method ) {
|
|
635
|
+
const verb = String( method || "" ).trim().toLowerCase();
|
|
636
|
+
return TiWebServer.#REGISTRABLE_METHODS.has( verb ) ? verb : null;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Tests a request path against a list of unprotected-route patterns (string exact-match or RegExp test),
|
|
641
|
+
* returning true on the first match. A RegExp's `lastIndex` is reset defensively so a stateful 'g'/'y' flag
|
|
642
|
+
* cannot cause a match to be skipped. Pure and static; shared by {@link TiWebServer#isUnprotectedRoute} and
|
|
643
|
+
* exposed for unit testing — not part of the customization surface.
|
|
644
|
+
*
|
|
645
|
+
* @method
|
|
646
|
+
* @static
|
|
647
|
+
* @param {Array<string|RegExp>} patterns
|
|
648
|
+
* @param {string} pathOnly The request path with any query string already stripped.
|
|
649
|
+
* @returns {boolean}
|
|
650
|
+
* @public
|
|
651
|
+
*/
|
|
652
|
+
static isRouteInList( patterns, pathOnly ) {
|
|
653
|
+
for ( let idx = 0; idx < patterns.length; idx++ ) {
|
|
654
|
+
const pattern = patterns[ idx ];
|
|
655
|
+
let matched;
|
|
656
|
+
if ( _.isRegExp( pattern ) ) {
|
|
657
|
+
// Avoid stateful RegExp behavior when 'g' or 'y' flags are present:
|
|
658
|
+
pattern.lastIndex = 0;
|
|
659
|
+
matched = pattern.test( pathOnly );
|
|
660
|
+
} else {
|
|
661
|
+
matched = ( pattern === pathOnly );
|
|
662
|
+
}
|
|
663
|
+
if ( matched === true ) {
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
|
|
577
670
|
/* Private interface */
|
|
578
671
|
|
|
579
672
|
/**
|
|
@@ -1,60 +1,64 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* The ti-engine is an open source, free to use—both for personal and commercial projects—framework for the creation of microservice-based solutions using node.js.
|
|
3
|
-
* Copyright © 2021-2026 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
4
|
-
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
5
|
-
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
6
|
-
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
"use strict";
|
|
10
|
-
|
|
11
|
-
const tools = require( "@ti-engine/core/tools" );
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Applies TI_WEB_* environment-variable overrides onto an (already-merged) web server configuration object.
|
|
15
|
-
* Each override is applied ONLY when its environment variable is defined, so an absent variable leaves the
|
|
16
|
-
* configured/default value untouched (fully backward compatible). This gives ti-engine web servers 12-factor,
|
|
17
|
-
* container-friendly control over network binding, TLS, the session cookie secret,
|
|
18
|
-
* methods without editing config files. Note `TI_WEB_AUTH_METHODS`
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* @
|
|
23
|
-
* @param {Object}
|
|
24
|
-
* @
|
|
25
|
-
* @
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
config.cookies
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
config.auth
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
1
|
+
/*
|
|
2
|
+
* The ti-engine is an open source, free to use—both for personal and commercial projects—framework for the creation of microservice-based solutions using node.js.
|
|
3
|
+
* Copyright © 2021-2026 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
5
|
+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
6
|
+
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
"use strict";
|
|
10
|
+
|
|
11
|
+
const tools = require( "@ti-engine/core/tools" );
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Applies TI_WEB_* environment-variable overrides onto an (already-merged) web server configuration object.
|
|
15
|
+
* Each override is applied ONLY when its environment variable is defined, so an absent variable leaves the
|
|
16
|
+
* configured/default value untouched (fully backward compatible). This gives ti-engine web servers 12-factor,
|
|
17
|
+
* container-friendly control over network binding, TLS, the session cookie secret, the enabled authentication
|
|
18
|
+
* methods, and the trusted request origins without editing config files. Note `TI_WEB_AUTH_METHODS` and
|
|
19
|
+
* `TI_WEB_TRUSTED_ORIGINS` fully REPLACE their config arrays (`auth.enabledMethods` / `trustedOrigins`) rather than
|
|
20
|
+
* merging — the config-file merge is by-index and cannot cleanly override an array.
|
|
21
|
+
*
|
|
22
|
+
* @method
|
|
23
|
+
* @param {Object} config The web server configuration to augment (mutated in place and returned).
|
|
24
|
+
* @param {Object} [env=process.env] The environment source (injectable for testing).
|
|
25
|
+
* @returns {Object} The same config object, with any present overrides applied.
|
|
26
|
+
* @public
|
|
27
|
+
*/
|
|
28
|
+
function applyWebConfigEnvOverrides( config, env = process.env ) {
|
|
29
|
+
if ( !config || typeof config !== "object" ) {
|
|
30
|
+
return config;
|
|
31
|
+
}
|
|
32
|
+
if ( env.TI_WEB_HOST !== undefined ) {
|
|
33
|
+
config.host = env.TI_WEB_HOST;
|
|
34
|
+
}
|
|
35
|
+
if ( env.TI_WEB_PORT !== undefined ) {
|
|
36
|
+
const port = Number( env.TI_WEB_PORT );
|
|
37
|
+
if ( Number.isInteger( port ) ) {
|
|
38
|
+
config.port = port;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if ( env.TI_WEB_USE_TLS !== undefined ) {
|
|
42
|
+
config.useTLS = tools.toBool( env.TI_WEB_USE_TLS );
|
|
43
|
+
}
|
|
44
|
+
if ( env.TI_WEB_TLS_CERT_PATH !== undefined ) {
|
|
45
|
+
config.tlsCertPath = env.TI_WEB_TLS_CERT_PATH;
|
|
46
|
+
}
|
|
47
|
+
if ( env.TI_WEB_TLS_KEY_PATH !== undefined ) {
|
|
48
|
+
config.tlsKeyPath = env.TI_WEB_TLS_KEY_PATH;
|
|
49
|
+
}
|
|
50
|
+
if ( env.TI_WEB_COOKIE_SECRET !== undefined ) {
|
|
51
|
+
config.cookies = config.cookies || {};
|
|
52
|
+
config.cookies.secret = env.TI_WEB_COOKIE_SECRET;
|
|
53
|
+
}
|
|
54
|
+
if ( env.TI_WEB_AUTH_METHODS !== undefined ) {
|
|
55
|
+
config.auth = config.auth || {};
|
|
56
|
+
config.auth.enabledMethods = env.TI_WEB_AUTH_METHODS.split( "," ).map( ( method ) => method.trim() ).filter( ( method ) => method.length > 0 );
|
|
57
|
+
}
|
|
58
|
+
if ( env.TI_WEB_TRUSTED_ORIGINS !== undefined ) {
|
|
59
|
+
config.trustedOrigins = env.TI_WEB_TRUSTED_ORIGINS.split( "," ).map( ( origin ) => origin.trim() ).filter( ( origin ) => origin.length > 0 );
|
|
60
|
+
}
|
|
61
|
+
return config;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = applyWebConfigEnvOverrides;
|
|
@@ -690,19 +690,30 @@ module.exports.webAppHandler = ( instance ) => {
|
|
|
690
690
|
* @returns {ExpressHandler}
|
|
691
691
|
* @public
|
|
692
692
|
*/
|
|
693
|
-
module.exports.originRefererValidationHandler = () => {
|
|
693
|
+
module.exports.originRefererValidationHandler = ( instance ) => {
|
|
694
694
|
return ( request, response, next ) => {
|
|
695
695
|
if ( request.method === "GET" || request.method === "HEAD" || request.method === "OPTIONS" ) {
|
|
696
696
|
next();
|
|
697
697
|
} else {
|
|
698
|
-
const expectedOrigin = getBaseUrl( request );
|
|
699
698
|
const providedOrigin = getRequestOrigin( request );
|
|
700
|
-
// If the browser didn
|
|
701
|
-
if ( providedOrigin
|
|
702
|
-
logger.log( `Issue identified with origin/referer mismatch. Expected '${ expectedOrigin }', received '${ providedOrigin }'.`, logger.logSeverity.WARNING );
|
|
703
|
-
next( exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, null, exceptions.httpCode.C_403 ) );
|
|
704
|
-
} else {
|
|
699
|
+
// If the browser didn't send Origin/Referer (normal for same-origin form POSTs), let CSRF middleware handle protection instead of blocking here:
|
|
700
|
+
if ( !providedOrigin ) {
|
|
705
701
|
next();
|
|
702
|
+
} else {
|
|
703
|
+
// Accept the origin the server reconstructs from the request, plus any explicitly trusted origins
|
|
704
|
+
// (TI_WEB_TRUSTED_ORIGINS / config.trustedOrigins). The trusted list is needed behind proxies that do
|
|
705
|
+
// not present the external host to the app (e.g. GitHub Codespaces port forwarding), where the browser
|
|
706
|
+
// Origin cannot be reconstructed from the forwarded headers.
|
|
707
|
+
const configured = ( instance && instance.serviceConfig && Array.isArray( instance.serviceConfig.trustedOrigins ) ) ? instance.serviceConfig.trustedOrigins : [];
|
|
708
|
+
const allowedOrigins = [ getBaseUrl( request ) ].concat( configured );
|
|
709
|
+
const normalizedProvided = String( providedOrigin ).trim().toLowerCase();
|
|
710
|
+
const isAllowed = allowedOrigins.some( ( origin ) => String( origin ).trim().toLowerCase() === normalizedProvided );
|
|
711
|
+
if ( isAllowed ) {
|
|
712
|
+
next();
|
|
713
|
+
} else {
|
|
714
|
+
logger.log( `Issue identified with origin/referer mismatch. Received '${ providedOrigin }'; expected one of [ ${ allowedOrigins.join( ", " ) } ].`, logger.logSeverity.WARNING );
|
|
715
|
+
next( exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, null, exceptions.httpCode.C_403 ) );
|
|
716
|
+
}
|
|
706
717
|
}
|
|
707
718
|
}
|
|
708
719
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ti-engine/web-framework",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.0",
|
|
4
4
|
"description": "A web-framework based on the ti-engine. It provides a customizable ready-to-use web-server microservice and a set of tools for creating web applications. NOTICE: This is still a work in progress and the full architecture, design, and functionality are not available!",
|
|
5
5
|
"author": "Boris Kostadinov <kostadinov.boris@gmail.com>",
|
|
6
6
|
"license": "GPL-3.0-or-later",
|