@danieltmn/openbridge 0.1.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 +60 -0
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/bin/openbridge.js +18 -0
- package/docs/ARCHITECTURE.md +77 -0
- package/package.json +54 -0
- package/src/auth.js +204 -0
- package/src/bridge/bridge.js +2280 -0
- package/src/cli.js +791 -0
- package/src/config.js +108 -0
- package/src/log.js +22 -0
- package/src/paths.js +152 -0
- package/src/push.js +85 -0
- package/src/store/index.js +930 -0
- package/src/store/jsonfile.js +54 -0
- package/src/tunnel/index.js +141 -0
- package/src/web/assets/app.js +3939 -0
- package/src/web/assets/icons/icon-128x128.png +0 -0
- package/src/web/assets/icons/icon-144x144.png +0 -0
- package/src/web/assets/icons/icon-152x152.png +0 -0
- package/src/web/assets/icons/icon-192x192.png +0 -0
- package/src/web/assets/icons/icon-384x384.png +0 -0
- package/src/web/assets/icons/icon-512x512.png +0 -0
- package/src/web/assets/icons/icon-72x72.png +0 -0
- package/src/web/assets/icons/icon-96x96.png +0 -0
- package/src/web/assets/manifest.webmanifest +24 -0
- package/src/web/assets/sw.js +85 -0
- package/src/web/assets/themes/dark-plus.css +19 -0
- package/src/web/assets/themes/dark-red.css +18 -0
- package/src/web/assets/themes/default.css +18 -0
- package/src/web/assets/themes/github-light.css +19 -0
- package/src/web/assets/themes/high-contrast.css +18 -0
- package/src/web/assets/themes/index.json +15 -0
- package/src/web/assets/themes/light-plus.css +19 -0
- package/src/web/assets/themes/light.css +18 -0
- package/src/web/assets/themes/monokai.css +19 -0
- package/src/web/assets/themes/one-dark.css +19 -0
- package/src/web/assets/themes/solarized.css +18 -0
- package/src/web/assets/themes/terminal.css +28 -0
- package/src/web/assets/themes/themes.css +2 -0
- package/src/web/routes.js +871 -0
- package/src/web/server.js +130 -0
- package/src/web/templates/chat.html +1296 -0
- package/src/web/templates/login.html +144 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const http = require('node:http');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const fsp = require('node:fs/promises');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const auth = require('../auth');
|
|
8
|
+
const routes = require('./routes');
|
|
9
|
+
const log = require('../log');
|
|
10
|
+
|
|
11
|
+
const ASSETS = path.join(__dirname, 'assets');
|
|
12
|
+
|
|
13
|
+
const MIME = {
|
|
14
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
15
|
+
'.css': 'text/css; charset=utf-8',
|
|
16
|
+
'.json': 'application/json; charset=utf-8',
|
|
17
|
+
'.webmanifest': 'application/manifest+json; charset=utf-8',
|
|
18
|
+
'.png': 'image/png',
|
|
19
|
+
'.svg': 'image/svg+xml',
|
|
20
|
+
'.ico': 'image/x-icon',
|
|
21
|
+
'.woff2': 'font/woff2',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function contentTypeFor(file) {
|
|
25
|
+
return MIME[path.extname(file).toLowerCase()] || 'application/octet-stream';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function readBody(req, limit = 24 * 1024 * 1024) {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
const chunks = [];
|
|
31
|
+
let len = 0;
|
|
32
|
+
req.on('data', (c) => {
|
|
33
|
+
len += c.length;
|
|
34
|
+
if (len > limit) { req.destroy(); return; }
|
|
35
|
+
chunks.push(c);
|
|
36
|
+
});
|
|
37
|
+
req.on('end', () => resolve(Buffer.concat(chunks)));
|
|
38
|
+
req.on('error', () => resolve(Buffer.alloc(0)));
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseForm(buf) {
|
|
43
|
+
const out = {};
|
|
44
|
+
for (const part of buf.toString('utf8').split('&')) {
|
|
45
|
+
if (!part) continue;
|
|
46
|
+
const i = part.indexOf('=');
|
|
47
|
+
const k = decodeURIComponent((i < 0 ? part : part.slice(0, i)).replace(/\+/g, ' '));
|
|
48
|
+
const v = decodeURIComponent((i < 0 ? '' : part.slice(i + 1)).replace(/\+/g, ' '));
|
|
49
|
+
out[k] = v;
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function serveStatic(res, rel) {
|
|
55
|
+
const safe = path.normalize('/' + rel).replace(/^(\.\.[/\\])+/, '');
|
|
56
|
+
const full = path.join(ASSETS, safe);
|
|
57
|
+
if (!full.startsWith(ASSETS)) return false;
|
|
58
|
+
try {
|
|
59
|
+
const st = await fsp.stat(full);
|
|
60
|
+
if (!st.isFile()) return false;
|
|
61
|
+
res.writeHead(200, { 'Content-Type': contentTypeFor(full), 'Cache-Control': 'no-cache' });
|
|
62
|
+
fs.createReadStream(full).pipe(res);
|
|
63
|
+
return true;
|
|
64
|
+
} catch (e) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function securityHeaders(res) {
|
|
70
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
71
|
+
res.setHeader('Referrer-Policy', 'no-referrer');
|
|
72
|
+
res.setHeader('X-Frame-Options', 'DENY');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function handle(app, req, res) {
|
|
76
|
+
securityHeaders(res);
|
|
77
|
+
const u = new URL(req.url, 'http://localhost');
|
|
78
|
+
const pathname = u.pathname;
|
|
79
|
+
const method = req.method || 'GET';
|
|
80
|
+
const query = u.searchParams;
|
|
81
|
+
|
|
82
|
+
if (pathname === '/api.php' || pathname === '/api') {
|
|
83
|
+
const bodyBuf = method === 'POST' ? await readBody(req) : Buffer.alloc(0);
|
|
84
|
+
let body = {};
|
|
85
|
+
if (bodyBuf.length) {
|
|
86
|
+
try { body = JSON.parse(bodyBuf.toString('utf8')); } catch (e) { body = {}; }
|
|
87
|
+
}
|
|
88
|
+
return routes.handleApi({ app, req, res, method, query, body, bodyBuf });
|
|
89
|
+
}
|
|
90
|
+
if (pathname === '/login.php' || pathname === '/login') {
|
|
91
|
+
if (method === 'POST') {
|
|
92
|
+
const form = parseForm(await readBody(req));
|
|
93
|
+
return routes.handleLogin({ app, req, res, query, form });
|
|
94
|
+
}
|
|
95
|
+
return routes.handleLoginPage({ app, req, res, query });
|
|
96
|
+
}
|
|
97
|
+
if (pathname === '/logout.php' || pathname === '/logout') {
|
|
98
|
+
return routes.handleLogout({ app, req, res });
|
|
99
|
+
}
|
|
100
|
+
if (pathname === '/chat.php' || pathname === '/chat' || pathname === '/') {
|
|
101
|
+
return routes.handleChat({ app, req, res, query });
|
|
102
|
+
}
|
|
103
|
+
if (pathname === '/index.php' || pathname === '/index') {
|
|
104
|
+
return routes.handleIndex({ app, req, res });
|
|
105
|
+
}
|
|
106
|
+
if (pathname === '/app.js' || pathname === '/sw.js' || pathname === '/manifest.webmanifest'
|
|
107
|
+
|| pathname.startsWith('/themes/') || pathname.startsWith('/icons/')) {
|
|
108
|
+
if (await serveStatic(res, pathname)) return;
|
|
109
|
+
}
|
|
110
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
111
|
+
res.end('not found');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function createServer(app) {
|
|
115
|
+
const server = http.createServer((req, res) => {
|
|
116
|
+
const started = Date.now();
|
|
117
|
+
res.on('finish', () => {
|
|
118
|
+
if (res.statusCode >= 400) {
|
|
119
|
+
log.warn('[web] ' + req.method + ' ' + req.url + ' -> ' + res.statusCode + ' (' + (Date.now() - started) + ' ms)');
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
handle(app, req, res).catch((e) => {
|
|
123
|
+
try { auth.json(res, 500, { ok: false, error: 'error interno' }); } catch (e2) { /* nada */ }
|
|
124
|
+
log.error('[web]', (e && e.stack) || e);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
return server;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = { createServer };
|