@metric-im/administrate 1.0.0 → 1.0.2

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/README.md CHANGED
@@ -1,2 +1,28 @@
1
1
  # administrate
2
2
  Tools for site administration
3
+
4
+ ## Certify
5
+ Certify will detect the hostname of incoming ssl requests and present the certs. If the domain name does not have a valid cert it requests one from letsencrypt and saves the keys to $HOME/.metric-im/
6
+
7
+ ```javascript
8
+ import https from "https";
9
+ import { Certify } from '@metric-im/administrate';
10
+ const app = express();
11
+ const certify = await Certify.attach(app,{contactEmail:'me@there.com'});
12
+ // certify.SNI return {key: xxx, cert: ...} for the current domain and/or triggers a request for a cert
13
+ const https_server = https.createServer({...certify.SNI},app);
14
+ ```
15
+ >NOTE: Set the environment variable PROFILE=DEV to instruct acme to fetch test circuits. Use this when testing or you will be throttled.
16
+
17
+ ## Multisite
18
+ Multisite acts as a proxy service. It listens on port 80 (4080), 443 (4443) for all web traffic and routes to the designated service. A service is identified by domain name. multisite will look for a service matching the incoming domain name in the *sites* folder. It spawns the app found with npm start and assigns it an http port. All subsquent traffic for that domain are routed to this process.
19
+
20
+ Multisite can also launch a new instance of the current app with the sanitized domain name as the first argument. In this way the same code can run multiple named apps in separate silos.
21
+
22
+ ```javascript
23
+ const multiSite = MultiSite.attach(app);
24
+ ```
25
+ See the Roots project [Harness](https;//github.com/rootz-global/harness). This is a simple host for multisite. It expects symlinks to all the apps the server responds to by domain name.
26
+
27
+ ## Syncrhonize
28
+ Provides a web hook to github for manaing synchronization with a code branch.
package/config.mjs CHANGED
@@ -56,7 +56,7 @@ export class DomainConfig extends Config {
56
56
  this.domainConfigFile = join(this.domainConfigDir, 'config.ini');
57
57
  if (!fs.existsSync(this.domainConfigDir)) {
58
58
  fs.mkdirSync(this.domainConfigDir);
59
- fs.writeFileSync(this.domainConfigFile,JSON.stringify([]));
59
+ fs.writeFileSync(this.domainConfigFile,`name=${domain}`);
60
60
  }
61
61
  this.load();
62
62
  }
package/multisite.mjs CHANGED
@@ -18,6 +18,8 @@ export class MultiSite {
18
18
  this._spawnPort = (parseInt(this.options.spawnPort||0) || 53874);
19
19
  this.usedPorts = new Set();
20
20
  this.healthCheckIntervals = new Map();
21
+ this.isShuttingDown = false;
22
+ this.errorLogTracker = new Map(); // Track error frequency for rate limiting
21
23
  this.setupCleanup();
22
24
  }
23
25
  get spawnPort() {
@@ -27,13 +29,58 @@ export class MultiSite {
27
29
  }
28
30
 
29
31
  setupCleanup() {
30
- process.on('SIGTERM', () => this.cleanup());
31
- process.on('SIGINT', () => this.cleanup());
32
- process.on('exit', () => this.cleanup());
32
+ // Set up centralized signal handlers that host applications can use
33
+ this.gracefulShutdown = this.gracefulShutdown.bind(this);
34
+
35
+ // Only handle the 'exit' event for emergency cleanup
36
+ process.on('exit', () => {
37
+ // Synchronous cleanup only - no async operations allowed in 'exit'
38
+ console.log('Emergency cleanup on exit...');
39
+ Object.values(this.sites).forEach((site) => {
40
+ if (site.proc && !site.proc.killed) {
41
+ site.proc.kill('SIGKILL');
42
+ }
43
+ });
44
+ });
45
+ }
46
+
47
+ async gracefulShutdown(signal) {
48
+ if (this.isShuttingDown) {
49
+ console.log(`Received ${signal} again, forcing exit...`);
50
+ process.exit(1);
51
+ }
52
+
53
+ this.isShuttingDown = true;
54
+ console.log(`Received ${signal}, shutting down gracefully...`);
55
+
56
+ try {
57
+ console.log("Cleaning up child processes...");
58
+ await this.cleanup();
59
+
60
+ console.log("Graceful shutdown complete");
61
+ process.exit(0);
62
+ } catch (error) {
63
+ console.error("Error during shutdown:", error);
64
+ process.exit(1);
65
+ }
66
+ }
67
+
68
+ setupSignalHandlers() {
69
+ // Method host applications can call to set up proper signal handling
70
+ process.on('SIGINT', () => this.gracefulShutdown('SIGINT'));
71
+ process.on('SIGTERM', () => this.gracefulShutdown('SIGTERM'));
72
+ console.log('MultiSite signal handlers installed');
33
73
  }
34
74
 
35
75
  async cleanup() {
36
76
  console.log('Cleaning up spawned processes...');
77
+
78
+ // Set a global timeout for the entire cleanup process
79
+ const cleanupTimeout = setTimeout(() => {
80
+ console.log('Cleanup taking too long, forcing exit...');
81
+ process.exit(1);
82
+ }, 15000); // 15 second total timeout
83
+
37
84
  const promises = Object.values(this.sites).map(async (site) => {
38
85
  if (site.proc && !site.proc.killed) {
39
86
  return new Promise((resolve) => {
@@ -43,7 +90,7 @@ export class MultiSite {
43
90
  site.proc.kill('SIGKILL');
44
91
  }
45
92
  resolve();
46
- }, 10000); // 10 second timeout for graceful shutdown
93
+ }, 5000); // Reduced to 5 second timeout per process
47
94
 
48
95
  site.proc.kill('SIGTERM');
49
96
 
@@ -62,6 +109,7 @@ export class MultiSite {
62
109
  this.healthCheckIntervals.forEach(interval => clearInterval(interval));
63
110
  this.healthCheckIntervals.clear();
64
111
 
112
+ clearTimeout(cleanupTimeout);
65
113
  console.log('All spawned processes cleaned up');
66
114
  }
67
115
 
@@ -129,13 +177,44 @@ export class MultiSite {
129
177
  }
130
178
  });
131
179
  }
180
+
181
+ // Rate limit error logging to prevent spam
182
+ shouldLogError(target, clientIP) {
183
+ const key = `${clientIP}:${target}`;
184
+ const now = Date.now();
185
+ const logEntry = this.errorLogTracker.get(key);
186
+
187
+ if (!logEntry) {
188
+ this.errorLogTracker.set(key, { count: 1, firstSeen: now, lastLogged: now });
189
+ return true;
190
+ }
191
+
192
+ logEntry.count++;
193
+
194
+ // Reset counter if it's been more than 5 minutes since first error
195
+ if (now - logEntry.firstSeen > 300000) {
196
+ logEntry.count = 1;
197
+ logEntry.firstSeen = now;
198
+ logEntry.lastLogged = now;
199
+ return true;
200
+ }
201
+
202
+ // Log first error, then every 10th error, but not more than once per minute
203
+ if (logEntry.count === 1 ||
204
+ (logEntry.count % 10 === 0 && now - logEntry.lastLogged > 60000)) {
205
+ logEntry.lastLogged = now;
206
+ return true;
207
+ }
208
+
209
+ return false;
210
+ }
132
211
  static async attach(app,options) {
133
212
  const instance = new MultiSite(app,options);
134
213
  instance.config = new Config();
135
214
  // spawn declared sites
136
215
  if (fs.existsSync(resolve('./sites'))) {
137
216
  instance.sites = (fs.readdirSync(resolve('./sites'))).reduce((result,hostName)=>{
138
- const domainName = Site.GetId(hostName);
217
+ const domainName = Site.WashName(hostName);
139
218
  const options = {
140
219
  cwd: resolve(`./sites/${hostName}`),
141
220
  env: {PORT:instance.spawnPort,meta:instance.options}
@@ -151,13 +230,14 @@ export class MultiSite {
151
230
  const router = express.Router();
152
231
 
153
232
  router.all(/.*/, async (req, res) => {
154
- const domain = Site.GetId(req.hostname);
233
+ const domain = Site.WashName(req.hostname);
155
234
  const site = this.sites[domain];
156
235
  if (site) {
157
236
  let target = `http://127.0.0.1:${site.options.env.PORT}${req.url}`;
158
237
  const method = req.method;
159
238
  const isBodyMethod = ['POST', 'PUT', 'PATCH'].includes(method);
160
239
  const payload = isBodyMethod ? req.body : null;
240
+ const clientIP = req.ip || req.connection.remoteAddress || req.headers['x-forwarded-for'];
161
241
 
162
242
  // Debug logging for POST requests
163
243
  if (method === 'POST') {
@@ -188,14 +268,14 @@ export class MultiSite {
188
268
  timeout: 30000,
189
269
  responseType: 'stream'
190
270
  });
191
-
271
+
192
272
  console.log(`${response.status} ${target} (binary stream)`);
193
-
273
+
194
274
  // Set headers without the problematic ones
195
275
  const cleanHeaders = {...response.headers};
196
276
  delete cleanHeaders['transfer-encoding'];
197
277
  delete cleanHeaders['content-encoding'];
198
-
278
+
199
279
  res.status(response.status).set(cleanHeaders);
200
280
  response.data.pipe(res);
201
281
  } else {
@@ -210,17 +290,25 @@ export class MultiSite {
210
290
  timeout: 30000,
211
291
  responseType: 'text'
212
292
  });
213
-
293
+
214
294
  console.log(`${response.status} ${target}`);
215
-
295
+
216
296
  const cleanHeaders = {...response.headers};
217
297
  delete cleanHeaders['transfer-encoding'];
218
298
  delete cleanHeaders['content-encoding'];
219
-
299
+
220
300
  res.status(response.status).set(cleanHeaders).send(response.data);
221
301
  }
222
302
  } catch (error) {
223
- console.error(`[Proxy] Error connecting to ${target}:`, error.message);
303
+ // Rate limit error logging to prevent spam
304
+ if (this.shouldLogError(target, clientIP)) {
305
+ const logEntry = this.errorLogTracker.get(`${clientIP}:${target}`);
306
+ if (logEntry && logEntry.count > 1) {
307
+ console.error(`${clientIP}:E: [Proxy] Error connecting to ${target}: ${error.message} (${logEntry.count} times)`);
308
+ } else {
309
+ console.error(`${clientIP}:E: [Proxy] Error connecting to ${target}: ${error.message}`);
310
+ }
311
+ }
224
312
 
225
313
  // // Clean up dead processes
226
314
  // this.removeDeadSites();
@@ -319,9 +407,12 @@ export class Site {
319
407
  console.error(`${this.name}: Exception while spawning:`, err);
320
408
  this.proc = null;
321
409
  }
322
- }
323
- static GetId(hostName="") {
324
- if (hostName.match(/^[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}$/)) return null
325
- return hostName.toLowerCase().replace(/[^a-z0-9-]+/g,'_');
410
+ }
411
+ static WashName(hostName="") {
412
+ if (!hostName || hostName.match(/^[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}$/)) return "";
413
+ else return hostName.toLowerCase();
414
+ }
415
+ static SafeName(hostName) {
416
+ return Site.WashName(hostName).replace(/[^a-z0-9-]+/g,'_');
326
417
  }
327
418
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metric-im/administrate",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Tools for site administration",
5
5
  "homepage": "https://github.com/metric-im/administrate#readme",
6
6
  "bugs": {
@@ -1,10 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Read(//home/msprague/workspace/sitewell/**)",
5
- "Read(//home/msprague/workspace/rootz/data-wallet-demos/**)"
6
- ],
7
- "deny": [],
8
- "ask": []
9
- }
10
- }