@awesomeness-js/server 1.0.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/.editorconfig +4 -0
- package/.gitattributes +2 -0
- package/.jshintrc +3 -0
- package/.vscode/settings.json +17 -0
- package/CLA.md +9 -0
- package/CONTRIBUTING.md +18 -0
- package/LICENSE +13 -0
- package/NOTICE +15 -0
- package/README.md +15 -0
- package/SECURITY.md +7 -0
- package/config.js +29 -0
- package/eslint.config.js +101 -0
- package/example-.awesomeness/applicationMap.js +4 -0
- package/example-.awesomeness/beforeRouteMiddleware.js +15 -0
- package/example-.awesomeness/checkSession.js +15 -0
- package/example-.awesomeness/config.js +40 -0
- package/example-.awesomeness/hostMap.js +44 -0
- package/example-.awesomeness/initDB.js +65 -0
- package/example-.awesomeness/setupDB/applications.js +47 -0
- package/example-.awesomeness/setupDB/users.js +65 -0
- package/example-.awesomeness/setupDB/websites.js +49 -0
- package/example-.awesomeness/specialRoutes.js +7 -0
- package/example-.awesomeness/wsHandler.js +13 -0
- package/index.js +22 -0
- package/package.json +34 -0
- package/server/applicationMap.js +33 -0
- package/server/awesomenessNormalizeRequest.js +131 -0
- package/server/brotliJsonResponse.js +28 -0
- package/server/checkAccess.js +34 -0
- package/server/componentDependencies.js +301 -0
- package/server/errors.js +11 -0
- package/server/fetchPage.js +269 -0
- package/server/getMD.js +22 -0
- package/server/koa/attachAwesomenessRequest.js +24 -0
- package/server/koa/cors.js +22 -0
- package/server/koa/errorHandler.js +32 -0
- package/server/koa/finalFormat.js +34 -0
- package/server/koa/jsonBodyParser.js +172 -0
- package/server/koa/routeRequest.js +288 -0
- package/server/koa/serverUp.js +7 -0
- package/server/koa/staticFiles.js +97 -0
- package/server/koa/timeout.js +42 -0
- package/server/pageInfo.js +121 -0
- package/server/reRoute.js +54 -0
- package/server/resolveRealCasePath.js +56 -0
- package/server/specialPaths.js +107 -0
- package/server/validateRequest.js +127 -0
- package/server/ws/handlers.js +67 -0
- package/server/ws/index.js +50 -0
- package/start.js +122 -0
- package/vitest.config.js +15 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// cache: site -> pattern -> { regex, keys }
|
|
2
|
+
const __routeCache = new Map();
|
|
3
|
+
|
|
4
|
+
function compile(site, pattern) {
|
|
5
|
+
|
|
6
|
+
let siteMap = __routeCache.get(site);
|
|
7
|
+
|
|
8
|
+
if (!siteMap) {
|
|
9
|
+
|
|
10
|
+
siteMap = new Map();
|
|
11
|
+
__routeCache.set(site, siteMap);
|
|
12
|
+
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let compiled = siteMap.get(pattern);
|
|
16
|
+
|
|
17
|
+
if (!compiled) {
|
|
18
|
+
|
|
19
|
+
const esc = (s) => s.replace(/([.+^=!:${}()|[\]\\])/g, '\\$1');
|
|
20
|
+
const keys = [];
|
|
21
|
+
const re = '^/' + pattern
|
|
22
|
+
.split('/').filter(Boolean)
|
|
23
|
+
.map((seg) => {
|
|
24
|
+
|
|
25
|
+
if (seg.startsWith(':')) {
|
|
26
|
+
|
|
27
|
+
keys.push(seg.slice(1));
|
|
28
|
+
|
|
29
|
+
return '([^/]+)';
|
|
30
|
+
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return esc(seg);
|
|
34
|
+
|
|
35
|
+
})
|
|
36
|
+
.join('/') + '/?$';
|
|
37
|
+
|
|
38
|
+
compiled = {
|
|
39
|
+
regex: new RegExp(re),
|
|
40
|
+
keys
|
|
41
|
+
};
|
|
42
|
+
siteMap.set(pattern, compiled);
|
|
43
|
+
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return compiled;
|
|
47
|
+
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function matchPathSite(site, pattern, path) {
|
|
51
|
+
|
|
52
|
+
const {
|
|
53
|
+
regex, keys
|
|
54
|
+
} = compile(site, pattern);
|
|
55
|
+
const m = regex.exec(path);
|
|
56
|
+
|
|
57
|
+
if (!m) return null;
|
|
58
|
+
|
|
59
|
+
const params = {};
|
|
60
|
+
|
|
61
|
+
for (let i = 0; i < keys.length; i++) params[keys[i]] = m[i + 1];
|
|
62
|
+
|
|
63
|
+
return params;
|
|
64
|
+
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function specialPaths(awesomenessRequest, routes) {
|
|
68
|
+
|
|
69
|
+
for (const route of routes) {
|
|
70
|
+
|
|
71
|
+
// console.log('Checking route:', {
|
|
72
|
+
// pattern: route.pattern,
|
|
73
|
+
// path: awesomenessRequest.path,
|
|
74
|
+
// site: awesomenessRequest.site
|
|
75
|
+
// });
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
const params = matchPathSite(awesomenessRequest.site, route.pattern, awesomenessRequest.path);
|
|
79
|
+
|
|
80
|
+
if (params) {
|
|
81
|
+
|
|
82
|
+
// console.log('Matched route:', {
|
|
83
|
+
// pattern: route.pattern,
|
|
84
|
+
// path: awesomenessRequest.path,
|
|
85
|
+
// site: awesomenessRequest.site,
|
|
86
|
+
// params
|
|
87
|
+
// });
|
|
88
|
+
|
|
89
|
+
await route.handler(awesomenessRequest, params, route);
|
|
90
|
+
|
|
91
|
+
return; // stop the chain here
|
|
92
|
+
|
|
93
|
+
} else {
|
|
94
|
+
|
|
95
|
+
// console.log('No match for route:', {
|
|
96
|
+
// pattern: route.pattern,
|
|
97
|
+
// path: awesomenessRequest.path,
|
|
98
|
+
// site: awesomenessRequest.site
|
|
99
|
+
// });
|
|
100
|
+
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export default specialPaths;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { clean } from '@awesomeness-js/utils';
|
|
2
|
+
import checkAccess from './checkAccess.js';
|
|
3
|
+
import { getConfig } from "../config.js";
|
|
4
|
+
|
|
5
|
+
export default async (awesomenessRequest) => {
|
|
6
|
+
|
|
7
|
+
const awesomenessConfig = getConfig();
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
data,
|
|
11
|
+
routeInfo
|
|
12
|
+
} = awesomenessRequest;
|
|
13
|
+
|
|
14
|
+
// IS SESSION VALID
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
|
|
18
|
+
await awesomenessConfig.checkSession(awesomenessRequest);
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
} catch(e){
|
|
22
|
+
|
|
23
|
+
if(
|
|
24
|
+
routeInfo?.permissions.length
|
|
25
|
+
&& !routeInfo?.permissions.includes('*')
|
|
26
|
+
){
|
|
27
|
+
|
|
28
|
+
if(
|
|
29
|
+
process.env.NODE_ENV === 'development'
|
|
30
|
+
&& awesomenessConfig.byPassAccessRequirementsInDev === true
|
|
31
|
+
){
|
|
32
|
+
|
|
33
|
+
if(awesomenessConfig.debug){
|
|
34
|
+
|
|
35
|
+
console.log('by passing access requirement: NODE_ENV is development && byPassAccessRequirementsInDev is true');
|
|
36
|
+
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
} else {
|
|
40
|
+
|
|
41
|
+
throw {
|
|
42
|
+
message: 'session check failed',
|
|
43
|
+
error: e
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if(routeInfo?.permissions.length){
|
|
55
|
+
|
|
56
|
+
await checkAccess({
|
|
57
|
+
permissionsAllowed: routeInfo.permissions,
|
|
58
|
+
awesomenessRequest,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// DOES USER HAVE THE RIGHT PERMISSIONS
|
|
64
|
+
|
|
65
|
+
// CLEAN THE DATA
|
|
66
|
+
try {
|
|
67
|
+
|
|
68
|
+
if(!routeInfo.properties){
|
|
69
|
+
|
|
70
|
+
// nothing to clean
|
|
71
|
+
return true;
|
|
72
|
+
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if(typeof data === 'object' && Object.keys(data).length){
|
|
76
|
+
|
|
77
|
+
const testing = data.testing || false;
|
|
78
|
+
|
|
79
|
+
delete data.testing;
|
|
80
|
+
|
|
81
|
+
if(awesomenessConfig.debug){
|
|
82
|
+
|
|
83
|
+
console.log({ data });
|
|
84
|
+
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let cleanedObject = clean.object(data, routeInfo.properties, {
|
|
88
|
+
testMode: awesomenessConfig.debug,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
data.testing = testing;
|
|
93
|
+
|
|
94
|
+
if(!awesomenessRequest.cleanData){
|
|
95
|
+
|
|
96
|
+
awesomenessRequest.cleanData = {};
|
|
97
|
+
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// awesomenessRequest.cleanData = data;
|
|
101
|
+
|
|
102
|
+
awesomenessRequest.cleanData = {
|
|
103
|
+
... awesomenessRequest.cleanData,
|
|
104
|
+
... cleanedObject
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
return true;
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
} catch (err) {
|
|
115
|
+
|
|
116
|
+
throw {
|
|
117
|
+
message: 'validation error',
|
|
118
|
+
err: {
|
|
119
|
+
details: err,
|
|
120
|
+
message: err.message,
|
|
121
|
+
stack: err.stack
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { getConfig } from "../../config.js";
|
|
2
|
+
|
|
3
|
+
export function handleWsMessage({
|
|
4
|
+
socket,
|
|
5
|
+
raw
|
|
6
|
+
}) {
|
|
7
|
+
|
|
8
|
+
const awesomenessConfig = getConfig();
|
|
9
|
+
|
|
10
|
+
let msg;
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
|
|
14
|
+
msg = JSON.parse(raw.toString());
|
|
15
|
+
|
|
16
|
+
} catch (err) {
|
|
17
|
+
|
|
18
|
+
socket.send(JSON.stringify({
|
|
19
|
+
type: "error",
|
|
20
|
+
error: "Invalid JSON",
|
|
21
|
+
ts: Date.now()
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
return;
|
|
25
|
+
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (awesomenessConfig.debug) {
|
|
29
|
+
|
|
30
|
+
// Example: log which client sent the message
|
|
31
|
+
console.log("client", socket.clientId, "sent", msg);
|
|
32
|
+
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (msg?.type === "ping") {
|
|
36
|
+
|
|
37
|
+
socket.send(JSON.stringify({
|
|
38
|
+
type: "pong",
|
|
39
|
+
clientId: socket.clientId,
|
|
40
|
+
echo: msg.data ?? null,
|
|
41
|
+
ts: Date.now()
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
return;
|
|
45
|
+
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if(typeof awesomenessConfig.wsHandler === 'function'){
|
|
49
|
+
|
|
50
|
+
awesomenessConfig.wsHandler({
|
|
51
|
+
socket,
|
|
52
|
+
message: msg,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
} else {
|
|
56
|
+
|
|
57
|
+
socket.send(JSON.stringify({
|
|
58
|
+
type: msg.type ?? "echo",
|
|
59
|
+
clientId: socket.clientId,
|
|
60
|
+
echo: msg,
|
|
61
|
+
ts: Date.now()
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { WebSocketServer } from "ws";
|
|
2
|
+
import { handleWsMessage } from "./handlers.js";
|
|
3
|
+
|
|
4
|
+
export function attachWs(server, path = "/ws") {
|
|
5
|
+
|
|
6
|
+
// noServer mode so we own upgrades
|
|
7
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
8
|
+
|
|
9
|
+
wss.on("connection", (socket, req) => {
|
|
10
|
+
|
|
11
|
+
const clientId = Math.random().toString(36).slice(2, 8);
|
|
12
|
+
|
|
13
|
+
socket.clientId = clientId;
|
|
14
|
+
|
|
15
|
+
socket.send(JSON.stringify({
|
|
16
|
+
type: "welcome",
|
|
17
|
+
clientId,
|
|
18
|
+
ts: Date.now()
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
socket.on("message", (raw) => handleWsMessage({
|
|
22
|
+
socket,
|
|
23
|
+
raw,
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
server.on("upgrade", (req, socket, head) => {
|
|
29
|
+
|
|
30
|
+
const { url } = req;
|
|
31
|
+
|
|
32
|
+
if (url === path) {
|
|
33
|
+
|
|
34
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
35
|
+
|
|
36
|
+
wss.emit("connection", ws, req);
|
|
37
|
+
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
} else {
|
|
41
|
+
|
|
42
|
+
socket.destroy();
|
|
43
|
+
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return wss;
|
|
49
|
+
|
|
50
|
+
}
|
package/start.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import Koa from 'koa';
|
|
2
|
+
import compress from 'koa-compress';
|
|
3
|
+
import http from "http";
|
|
4
|
+
|
|
5
|
+
import { attachAwesomenessRequest } from './server/koa/attachAwesomenessRequest.js';
|
|
6
|
+
import { cors } from './server/koa/cors.js';
|
|
7
|
+
import { errorHandler } from './server/koa/errorHandler.js';
|
|
8
|
+
import { jsonBodyParser } from './server/koa/jsonBodyParser.js';
|
|
9
|
+
import { serverUp } from './server/koa/serverUp.js';
|
|
10
|
+
import { timeout } from './server/koa/timeout.js';
|
|
11
|
+
import { staticFiles } from "./server/koa/staticFiles.js";
|
|
12
|
+
import { routeRequest } from './server/koa/routeRequest.js';
|
|
13
|
+
import './server/errors.js'; // handle errors
|
|
14
|
+
|
|
15
|
+
import { attachWs } from "./server/ws/index.js";
|
|
16
|
+
|
|
17
|
+
import { getConfig } from './config.js';
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
export default async function start(){
|
|
21
|
+
|
|
22
|
+
const awesomenessConfig = getConfig();
|
|
23
|
+
|
|
24
|
+
console.log(`Starting Awesomeness server on port ${process.env.PORT}`);
|
|
25
|
+
|
|
26
|
+
// Koa app
|
|
27
|
+
const app = new Koa();
|
|
28
|
+
|
|
29
|
+
app.use(compress());
|
|
30
|
+
|
|
31
|
+
// error handling
|
|
32
|
+
app.use(errorHandler);
|
|
33
|
+
|
|
34
|
+
// limits
|
|
35
|
+
app.use(jsonBodyParser);
|
|
36
|
+
app.use(timeout(1000 * 60 * 5)); // 5 minutes
|
|
37
|
+
|
|
38
|
+
// enable cors
|
|
39
|
+
app.use(cors);
|
|
40
|
+
|
|
41
|
+
// where we going
|
|
42
|
+
app.use(attachAwesomenessRequest);
|
|
43
|
+
|
|
44
|
+
// default static files
|
|
45
|
+
app.use(staticFiles);
|
|
46
|
+
|
|
47
|
+
if(Array.isArray(awesomenessConfig.beforeRouteMiddleware)){
|
|
48
|
+
|
|
49
|
+
awesomenessConfig.beforeRouteMiddleware.forEach((middlewareFunction) => {
|
|
50
|
+
|
|
51
|
+
app.use(middlewareFunction);
|
|
52
|
+
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
} else {
|
|
56
|
+
|
|
57
|
+
if(awesomenessConfig.debug){
|
|
58
|
+
|
|
59
|
+
console.log('No customMiddleware array found in awesomenessConfig');
|
|
60
|
+
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
// dynamic routing
|
|
67
|
+
app.use(routeRequest);
|
|
68
|
+
|
|
69
|
+
// create HTTP server manually so WS can hook upgrade
|
|
70
|
+
|
|
71
|
+
const server = http.createServer(app.callback());
|
|
72
|
+
|
|
73
|
+
if(typeof awesomenessConfig.wsHandler === 'function'){
|
|
74
|
+
|
|
75
|
+
attachWs(server);
|
|
76
|
+
|
|
77
|
+
} else {
|
|
78
|
+
|
|
79
|
+
console.log('No wsHandler function found in awesomenessConfig');
|
|
80
|
+
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// init DB
|
|
84
|
+
if(typeof awesomenessConfig.initDB === 'function'){
|
|
85
|
+
|
|
86
|
+
await awesomenessConfig.initDB();
|
|
87
|
+
|
|
88
|
+
} else {
|
|
89
|
+
|
|
90
|
+
console.log('No initDB function found in awesomenessConfig');
|
|
91
|
+
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
server.listen(process.env.PORT, serverUp);
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
[ 'SIGINT', 'SIGTERM' ].forEach(async (signal) => {
|
|
99
|
+
|
|
100
|
+
process.on(signal, async () => {
|
|
101
|
+
|
|
102
|
+
console.log(`Received ${signal}, shutting down gracefully...`);
|
|
103
|
+
server.close(() => {
|
|
104
|
+
|
|
105
|
+
console.log('Server closed');
|
|
106
|
+
process.exit(0);
|
|
107
|
+
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
setTimeout(() => {
|
|
111
|
+
|
|
112
|
+
console.error('Force shutdown');
|
|
113
|
+
process.exit(1);
|
|
114
|
+
|
|
115
|
+
}, 5000);
|
|
116
|
+
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
}
|
package/vitest.config.js
ADDED