roster-server 1.3.0 → 1.4.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 +59 -11
- package/demo/www/example.com/index.js +5 -3
- package/demo/www/express.example.com/index.js +11 -0
- package/demo/www/sio.example.com/client.js +1 -1
- package/demo/www/sio.example.com/index.js +17 -11
- package/demo/www/subdomain.example.com/index.js +6 -8
- package/index.js +55 -80
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,10 +38,11 @@ Your project should look something like this:
|
|
|
38
38
|
|
|
39
39
|
```javascript
|
|
40
40
|
// /srv/roster/server.js
|
|
41
|
-
const Roster = require('roster-
|
|
41
|
+
const Roster = require('roster-server');
|
|
42
42
|
|
|
43
43
|
const options = {
|
|
44
44
|
maintainerEmail: 'admin@example.com',
|
|
45
|
+
greenlockConfigDir: '/srv/greenlock.d', // Path to your Greenlock configuration directory
|
|
45
46
|
wwwPath: '/srv/www', // Path to your 'www' directory (default: '../www')
|
|
46
47
|
staging: false // Set to true for Let's Encrypt staging environment
|
|
47
48
|
};
|
|
@@ -54,12 +55,59 @@ server.start();
|
|
|
54
55
|
|
|
55
56
|
Each domain should have its own folder under `www`, containing an `index.js` that exports a request handler function.
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
### Examples
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
I'll help analyze the example files shown. You have 3 different implementations demonstrating various ways to handle requests in RosterServer:
|
|
61
|
+
|
|
62
|
+
1. **Basic HTTP Handler**:
|
|
63
|
+
```javascript:demo/www/example.com/index.js
|
|
64
|
+
module.exports = (server) => {
|
|
65
|
+
return (req, res) => {
|
|
66
|
+
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
67
|
+
res.end('"Loco de pensar, queriendo entrar en razón, y el corazón tiene razones que la propia razón nunca entenderá."');
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
2. **Express App**:
|
|
73
|
+
```javascript:demo/www/express.example.com/index.js
|
|
74
|
+
const express = require('express');
|
|
75
|
+
|
|
76
|
+
module.exports = (server) => {
|
|
77
|
+
const app = express();
|
|
78
|
+
app.get('/', (req, res) => {
|
|
79
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
80
|
+
res.send('"Loco de pensar, queriendo entrar en razón, y el corazón tiene razones que la propia razón nunca entenderá."');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return app;
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
3. **Socket.IO Server**:
|
|
88
|
+
```javascript:demo/www/sio.example.com/index.js
|
|
89
|
+
const { Server } = require('socket.io');
|
|
90
|
+
|
|
91
|
+
module.exports = (server) => {
|
|
92
|
+
const io = new Server(server);
|
|
93
|
+
|
|
94
|
+
io.on('connection', (socket) => {
|
|
95
|
+
console.log('A user connected');
|
|
96
|
+
|
|
97
|
+
socket.on('chat:message', (msg) => {
|
|
98
|
+
console.log('Message received:', msg);
|
|
99
|
+
io.emit('chat:message', msg);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
socket.on('disconnect', () => {
|
|
103
|
+
console.log('User disconnected');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return (req, res) => {
|
|
108
|
+
res.writeHead(200);
|
|
109
|
+
res.end('Socket.IO server running');
|
|
110
|
+
};
|
|
63
111
|
};
|
|
64
112
|
```
|
|
65
113
|
|
|
@@ -76,7 +124,7 @@ And that's it! Your server is now hosting multiple HTTPS-enabled sites. 🎉
|
|
|
76
124
|
|
|
77
125
|
### Automatic SSL Certificate Management
|
|
78
126
|
|
|
79
|
-
|
|
127
|
+
RosterServer uses [greenlock-express](https://www.npmjs.com/package/greenlock-express) to automatically obtain and renew SSL certificates from Let's Encrypt. No need to manually manage certificates ever again. Unless you enjoy that sort of thing. 🧐
|
|
80
128
|
|
|
81
129
|
### Redirects from `www`
|
|
82
130
|
|
|
@@ -84,11 +132,11 @@ All requests to `www.yourdomain.com` are automatically redirected to `yourdomain
|
|
|
84
132
|
|
|
85
133
|
### Dynamic Site Loading
|
|
86
134
|
|
|
87
|
-
Add a new site? Just drop it into the `www` folder with an `index.js` file, and
|
|
135
|
+
Add a new site? Just drop it into the `www` folder with an `index.js` file, and RosterServer will handle the rest. No need to restart the server. Well, you might need to restart the server. But that's what `nodemon` is for, right? 😅
|
|
88
136
|
|
|
89
137
|
## ⚙️ Configuration Options
|
|
90
138
|
|
|
91
|
-
When creating a new `
|
|
139
|
+
When creating a new `RosterServer` instance, you can pass the following options:
|
|
92
140
|
|
|
93
141
|
- `maintainerEmail` (string): Your email for Let's Encrypt notifications.
|
|
94
142
|
- `wwwPath` (string): Path to your `www` directory containing your sites.
|
|
@@ -97,14 +145,14 @@ When creating a new `RosterExpress` instance, you can pass the following options
|
|
|
97
145
|
|
|
98
146
|
## 🧂 A Touch of Magic
|
|
99
147
|
|
|
100
|
-
You might be thinking, "But setting up HTTPS and virtual hosts is supposed to be complicated and time-consuming!" Well, not anymore. With
|
|
148
|
+
You might be thinking, "But setting up HTTPS and virtual hosts is supposed to be complicated and time-consuming!" Well, not anymore. With RosterServer, you can get back to writing code that matters, like defending Earth from alien invaders! 👾👾👾
|
|
101
149
|
|
|
102
150
|
|
|
103
151
|
## 🤝 Contributing
|
|
104
152
|
|
|
105
153
|
Feel free to submit issues or pull requests. Or don't. I'm not your boss. 😜
|
|
106
154
|
|
|
107
|
-
If you find any issues or have suggestions for improvement, please open an issue or submit a pull request on the [GitHub repository](https://github.com/clasen/
|
|
155
|
+
If you find any issues or have suggestions for improvement, please open an issue or submit a pull request on the [GitHub repository](https://github.com/clasen/RosterServer).
|
|
108
156
|
|
|
109
157
|
## 🙏 Acknowledgments
|
|
110
158
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
module.exports = (
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
module.exports = (server) => {
|
|
2
|
+
return (req, res) => {
|
|
3
|
+
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
4
|
+
res.end('"Loco de pensar, queriendo entrar en razón, y el corazón tiene razones que la propia razón nunca entenderá."');
|
|
5
|
+
};
|
|
4
6
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
|
|
3
|
+
module.exports = (server) => {
|
|
4
|
+
const app = express();
|
|
5
|
+
app.get('/', (req, res) => {
|
|
6
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
7
|
+
res.send('"Loco de pensar, queriendo entrar en razón, y el corazón tiene razones que la propia razón nunca entenderá."');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
return app;
|
|
11
|
+
}
|
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
const { Server } = require('socket.io');
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
module.exports = (server) => {
|
|
4
|
+
const io = new Server(server);
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
io.on('connection', (socket) => {
|
|
7
|
+
console.log('A user connected');
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
socket.on('chat:message', (msg) => {
|
|
10
|
+
console.log('Message received:', msg);
|
|
11
|
+
io.emit('chat:message', msg);
|
|
12
|
+
});
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
socket.on('disconnect', () => {
|
|
15
|
+
console.log('User disconnected');
|
|
16
|
+
});
|
|
15
17
|
});
|
|
16
|
-
});
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
// Devolvemos el handler para las peticiones HTTP
|
|
20
|
+
return (req, res) => {
|
|
21
|
+
res.writeHead(200);
|
|
22
|
+
res.end('Socket.IO server running');
|
|
23
|
+
};
|
|
24
|
+
};
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
module.exports = server;
|
|
1
|
+
module.exports = (server) => {
|
|
2
|
+
return (req, res) => {
|
|
3
|
+
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
4
|
+
res.end('"Loco de pensar, queriendo entrar en razón, y el corazón tiene razones que la propia razón nunca entenderá."');
|
|
5
|
+
};
|
|
6
|
+
};
|
package/index.js
CHANGED
|
@@ -7,19 +7,18 @@ class Roster {
|
|
|
7
7
|
this.maintainerEmail = options.maintainerEmail || 'admin@example.com';
|
|
8
8
|
this.wwwPath = options.wwwPath || path.join(__dirname, '..', '..', '..', 'www');
|
|
9
9
|
this.greenlockConfigDir = options.greenlockConfigDir || path.join(__dirname, '..', '..', 'greenlock.d');
|
|
10
|
-
this.staging = options.staging || false;
|
|
10
|
+
this.staging = options.staging || false;
|
|
11
11
|
this.domains = [];
|
|
12
12
|
this.sites = {};
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
// Function to dynamically load domain applications
|
|
16
15
|
loadSites() {
|
|
17
16
|
fs.readdirSync(this.wwwPath, { withFileTypes: true })
|
|
18
17
|
.filter(dirent => dirent.isDirectory())
|
|
19
18
|
.forEach((dirent) => {
|
|
20
19
|
const domain = dirent.name;
|
|
21
20
|
const domainPath = path.join(this.wwwPath, domain);
|
|
22
|
-
|
|
21
|
+
|
|
23
22
|
const possibleIndexFiles = ['index.js', 'index.mjs', 'index.cjs'];
|
|
24
23
|
let siteApp;
|
|
25
24
|
let loadedFile;
|
|
@@ -27,8 +26,7 @@ class Roster {
|
|
|
27
26
|
for (const indexFile of possibleIndexFiles) {
|
|
28
27
|
const indexPath = path.join(domainPath, indexFile);
|
|
29
28
|
if (fs.existsSync(indexPath)) {
|
|
30
|
-
|
|
31
|
-
siteApp = typeof module === 'function' ? module() : module;
|
|
29
|
+
siteApp = require(indexPath);
|
|
32
30
|
loadedFile = indexFile;
|
|
33
31
|
break;
|
|
34
32
|
}
|
|
@@ -42,9 +40,6 @@ class Roster {
|
|
|
42
40
|
});
|
|
43
41
|
|
|
44
42
|
console.log(`✅ Loaded site: ${domain} (using ${loadedFile})`);
|
|
45
|
-
if (siteApp.attach) {
|
|
46
|
-
console.log(`🔌 Attachable server detected for ${domain}`);
|
|
47
|
-
}
|
|
48
43
|
} else {
|
|
49
44
|
console.warn(`⚠️ No index file (js/mjs/cjs) found in ${domainPath}`);
|
|
50
45
|
}
|
|
@@ -55,7 +50,6 @@ class Roster {
|
|
|
55
50
|
const configDir = this.greenlockConfigDir;
|
|
56
51
|
const configPath = path.join(configDir, 'config.json');
|
|
57
52
|
|
|
58
|
-
// Create the directory if it does not exist
|
|
59
53
|
if (!fs.existsSync(configDir)) {
|
|
60
54
|
fs.mkdirSync(configDir, { recursive: true });
|
|
61
55
|
}
|
|
@@ -68,10 +62,8 @@ class Roster {
|
|
|
68
62
|
uniqueDomains.add(rootDomain);
|
|
69
63
|
});
|
|
70
64
|
|
|
71
|
-
// Read the existing config.json if it exists
|
|
72
65
|
let existingConfig = {};
|
|
73
66
|
if (fs.existsSync(configPath)) {
|
|
74
|
-
// Read the current content
|
|
75
67
|
const currentConfigContent = fs.readFileSync(configPath, 'utf8');
|
|
76
68
|
existingConfig = JSON.parse(currentConfigContent);
|
|
77
69
|
}
|
|
@@ -82,7 +74,6 @@ class Roster {
|
|
|
82
74
|
altnames.push(`www.${domain}`);
|
|
83
75
|
}
|
|
84
76
|
|
|
85
|
-
// Find the existing site to preserve renewAt
|
|
86
77
|
let existingSite = null;
|
|
87
78
|
if (existingConfig.sites) {
|
|
88
79
|
existingSite = existingConfig.sites.find(site => site.subject === domain);
|
|
@@ -93,7 +84,6 @@ class Roster {
|
|
|
93
84
|
altnames: altnames
|
|
94
85
|
};
|
|
95
86
|
|
|
96
|
-
// Preserve renewAt if it exists
|
|
97
87
|
if (existingSite && existingSite.renewAt) {
|
|
98
88
|
siteConfig.renewAt = existingSite.renewAt;
|
|
99
89
|
}
|
|
@@ -121,34 +111,29 @@ class Roster {
|
|
|
121
111
|
sites: sitesConfig
|
|
122
112
|
};
|
|
123
113
|
|
|
124
|
-
// Check if config.json already exists and compare
|
|
125
114
|
if (fs.existsSync(configPath)) {
|
|
126
|
-
// Read the current content
|
|
127
115
|
const currentConfigContent = fs.readFileSync(configPath, 'utf8');
|
|
128
116
|
const currentConfig = JSON.parse(currentConfigContent);
|
|
129
117
|
|
|
130
|
-
// Compare the entire configurations
|
|
131
118
|
const newConfigContent = JSON.stringify(newConfig, null, 2);
|
|
132
119
|
const currentConfigContentFormatted = JSON.stringify(currentConfig, null, 2);
|
|
133
120
|
|
|
134
121
|
if (newConfigContent === currentConfigContentFormatted) {
|
|
135
122
|
console.log('ℹ️ Configuration has not changed. config.json will not be overwritten.');
|
|
136
|
-
return;
|
|
137
|
-
} else {
|
|
138
|
-
console.log('🔄 Configuration has changed. config.json will be updated.');
|
|
123
|
+
return;
|
|
139
124
|
}
|
|
125
|
+
console.log('🔄 Configuration has changed. config.json will be updated.');
|
|
140
126
|
} else {
|
|
141
127
|
console.log('🆕 config.json does not exist. A new one will be created.');
|
|
142
128
|
}
|
|
143
129
|
|
|
144
|
-
// Write the new config.json
|
|
145
130
|
fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2));
|
|
146
131
|
console.log(`📁 config.json generated at ${configPath}`);
|
|
147
132
|
}
|
|
148
133
|
|
|
149
134
|
handleRequest(req, res) {
|
|
150
135
|
const host = req.headers.host || '';
|
|
151
|
-
|
|
136
|
+
|
|
152
137
|
if (host.startsWith('www.')) {
|
|
153
138
|
const newHost = host.slice(4);
|
|
154
139
|
res.writeHead(301, { Location: `https://${newHost}${req.url}` });
|
|
@@ -158,79 +143,69 @@ class Roster {
|
|
|
158
143
|
|
|
159
144
|
const siteApp = this.sites[host];
|
|
160
145
|
if (siteApp) {
|
|
161
|
-
|
|
162
|
-
// Para servidores attachables (como Socket.IO)
|
|
163
|
-
res.writeHead(200);
|
|
164
|
-
res.end('Server running');
|
|
165
|
-
} else if (siteApp.emit) {
|
|
166
|
-
// Para servidores http/https (como subdomain.example.com)
|
|
167
|
-
siteApp.emit('request', req, res);
|
|
168
|
-
} else {
|
|
169
|
-
// Para funciones de manejo directo (como example.com)
|
|
170
|
-
siteApp(req, res);
|
|
171
|
-
}
|
|
146
|
+
siteApp(req, res);
|
|
172
147
|
} else {
|
|
173
148
|
res.writeHead(404);
|
|
174
149
|
res.end('Site not found');
|
|
175
150
|
}
|
|
176
151
|
}
|
|
177
152
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
maintainerEmail: this.maintainerEmail,
|
|
183
|
-
cluster: false,
|
|
184
|
-
staging: this.staging,
|
|
185
|
-
manager: { module: "@greenlock/manager" },
|
|
186
|
-
approveDomains: (opts, certs, cb) => {
|
|
187
|
-
// If certs is defined, we already have a certificate and are renewing it
|
|
188
|
-
if (certs) {
|
|
189
|
-
opts.domains = certs.altnames;
|
|
190
|
-
} else {
|
|
191
|
-
// If it's a new request, verify if the domain is in our list
|
|
192
|
-
if (this.domains.includes(opts.domain)) {
|
|
193
|
-
opts.email = this.maintainerEmail;
|
|
194
|
-
opts.agreeTos = true;
|
|
195
|
-
opts.domains = [opts.domain];
|
|
196
|
-
} else {
|
|
197
|
-
console.warn(`⚠️ Domain not approved: ${opts.domain}`);
|
|
198
|
-
return cb(new Error(`Domain not approved: ${opts.domain}`));
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
cb(null, { options: opts, certs });
|
|
202
|
-
}
|
|
203
|
-
}).ready((glx) => {
|
|
204
|
-
// Setup HTTPS server
|
|
205
|
-
const httpsServer = glx.httpsServer(null, (req, res) => {
|
|
206
|
-
this.handleRequest(req, res);
|
|
207
|
-
});
|
|
153
|
+
initServers(glx) {
|
|
154
|
+
const app = (req, res) => {
|
|
155
|
+
this.handleRequest(req, res);
|
|
156
|
+
};
|
|
208
157
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
158
|
+
// Obtener los servidores sin iniciarlos
|
|
159
|
+
const httpsServer = glx.httpsServer(null, app);
|
|
160
|
+
const httpServer = glx.httpServer();
|
|
161
|
+
|
|
162
|
+
// Inicializar las aplicaciones Socket.IO con el servidor HTTPS
|
|
163
|
+
for (const [host, siteApp] of Object.entries(this.sites)) {
|
|
164
|
+
if (!host.startsWith('www.')) {
|
|
165
|
+
const appInstance = siteApp(httpsServer);
|
|
166
|
+
this.sites[host] = appInstance;
|
|
167
|
+
this.sites[`www.${host}`] = appInstance;
|
|
168
|
+
console.log(`🔧 Initialized server for ${host}`);
|
|
215
169
|
}
|
|
170
|
+
}
|
|
216
171
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
// Setup HTTP server for ACME challenges
|
|
222
|
-
const httpServer = glx.httpServer();
|
|
223
|
-
|
|
224
|
-
httpServer.listen(80, "0.0.0.0", () => {
|
|
225
|
-
console.info("ℹ️ HTTP Listening on", httpServer.address());
|
|
226
|
-
});
|
|
227
|
-
});
|
|
172
|
+
// Retornar los servidores para iniciarlos después
|
|
173
|
+
return { httpsServer, httpServer };
|
|
228
174
|
}
|
|
229
175
|
|
|
230
176
|
start() {
|
|
231
177
|
this.loadSites();
|
|
232
178
|
this.generateConfigJson();
|
|
233
|
-
|
|
179
|
+
|
|
180
|
+
const greenlock = Greenlock.init({
|
|
181
|
+
packageRoot: __dirname,
|
|
182
|
+
configDir: this.greenlockConfigDir,
|
|
183
|
+
maintainerEmail: this.maintainerEmail,
|
|
184
|
+
cluster: false,
|
|
185
|
+
staging: this.staging
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Usar una promesa para manejar la inicialización
|
|
189
|
+
return new Promise((resolve, reject) => {
|
|
190
|
+
try {
|
|
191
|
+
greenlock.ready((glx) => {
|
|
192
|
+
const { httpsServer, httpServer } = this.initServers(glx);
|
|
193
|
+
|
|
194
|
+
// Primero iniciar el servidor HTTPS
|
|
195
|
+
httpsServer.listen(443, '0.0.0.0', () => {
|
|
196
|
+
console.log('ℹ️ HTTPS server listening on port 443');
|
|
197
|
+
|
|
198
|
+
// Después iniciar el servidor HTTP
|
|
199
|
+
httpServer.listen(80, '0.0.0.0', () => {
|
|
200
|
+
console.log('ℹ️ HTTP server listening on port 80');
|
|
201
|
+
resolve({ httpsServer, httpServer });
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
} catch (error) {
|
|
206
|
+
reject(error);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
234
209
|
}
|
|
235
210
|
}
|
|
236
211
|
|