Skip to content

Commit aa6ab18

Browse files
committed
Optional Web Admin Panel
1 parent a550d20 commit aa6ab18

File tree

7 files changed

+415
-1
lines changed

7 files changed

+415
-1
lines changed

README.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,35 @@ node bouncer.js somefile.conf &
6969
```
7070

7171
#### Keep it running forever (no downtime)
72+
### immortal
7273
To keep things running 24/7/365, there's a great app called [immortal](https://immortal.run/).
7374

7475
The immortaldir files are located in this repo (jbnc.yml).
7576

7677
Note: To use immortal on ubuntu, after following the steps on the page, please be sure to `systemctl enable immortaldir` as well as start.
7778

79+
### pm2
80+
Alternatively, there's pm2. To install:
81+
```
82+
npm install pm2 -g
83+
```
84+
To prevent logs from becoming too large, simply install:
85+
```
86+
pm2 install pm2-logrotate
87+
```
88+
No configuration is needed. It works as soon as any application is launched.
89+
90+
To start jbnc:
91+
```
92+
cd /home/folder/jbnc
93+
pm2 start bouncer.js
94+
or sudo pm2 start bouncer.js (if using SSL certificates from Let's Encrypt directories in /etc)
95+
```
96+
To stop:
97+
```
98+
Replace "start" with "stop"
99+
```
100+
78101
### IRC Client
79102
You just need to set your password in your jbnc config and then setup your IRC client:
80103
Just put this in your password:
@@ -142,6 +165,50 @@ SomePassword/buffername
142165
An example buffername could be 'desktop' and on the mobile phone could be 'mobile.'
143166

144167

168+
### Web Admin Panel
169+
170+
A web panel for an administrator is now integrated with jbnc. It is optional.
171+
To use it, simply install `npm install express express-session` and add the following to the jbnc.conf file:
172+
```
173+
"WebAdminPanel": true,
174+
"WebAdminPanelPort": 8889,
175+
"WebAdminPanelSecret":"<a_randomly_invented_key>",
176+
"WebAdminPanelPassword":"<password>",
177+
```
178+
179+
Then launch the web page in the browser at `http://127.0.0.1:8889`
180+
181+
The web page is designed to be used with a proxy pass from nginx or httpd.
182+
183+
For nginx, something like:
184+
185+
```
186+
server {
187+
...
188+
location /adminpanel {
189+
proxy_pass http://127.0.0.1:8889;
190+
...
191+
}
192+
...
193+
}
194+
```
195+
And for httpd:
196+
197+
```
198+
<VirtualHost *:443>
199+
...
200+
ProxyPass /adminpanel http://127.0.0.1:8889
201+
ProxyPassReverse /adminpanel http://127.0.0.1:8889
202+
...
203+
</VirtualHost>
204+
```
205+
206+
It is recommended to run the web page on an HTTPS-enabled site since both CDNs are HTTPS-enabled.
207+
208+
209+
###########
210+
211+
145212
### Copyright
146213

147214
(c) 2020 Andrew Lee <andrew@imperialfamily.com>

bouncer.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// jbnc v0.9.0
1+
// jbnc v0.9.1
22
// Copyright (C) 2020 Andrew Lee <andrew@imperialfamily.com>
33
// All Rights Reserved.
44
const fs = require('fs');
@@ -39,10 +39,17 @@ global.DEBUG = config.debug ? config.debug : false;
3939
global.WEBIRCSPECIAL = config.webircSpecial ? config.webircSpecial : false;
4040
global.IRC_STANDARDS = config.ircStandards ? config.ircStandards : true;
4141
global.UNCAUGHTEXCEPTION = config.uncaughtException ? config.uncaughtException : true;
42+
global.WEBADMINPANEL = config.WebAdminPanel ? config.WebAdminPanel : false;
43+
global.WEBADMINPANEL_PORT = config.WebAdminPanelPort ? config.WebAdminPanelPort : 8889;
44+
global.WEBADMINPANEL_PASSWORD = config.WebAdminPanelPassword ? config.WebAdminPanelPassword : false;
45+
global.WEBADMINPANEL_SECRET = config.WebAdminPanelSecret ? config.WebAdminPanelSecret : 'keyboard cat';
4246

4347
global.ircCommandList = new Set(["JOIN", "PART", "QUIT", "MODE", "PING", "NICK", "KICK"]);
4448
global.ircCommandRedistributeMessagesOnConnect = new Set(["AWAY", "NICK", "ACCOUNT", "PART", "QUIT", "MODE", "KICK", "TOPIC"]);
4549

50+
global.LAST_LAUNCH = new Date().toLocaleString();
51+
global.LAST_BUG = 'none';
52+
4653

4754
// Reload passwords on sighup
4855
process.on('SIGHUP', function () {
@@ -59,6 +66,7 @@ if (global.UNCAUGHTEXCEPTION) {
5966
// Prevent BNC from crashing for all other users when an error is caused by a user (with log error and time)
6067
process.on('uncaughtException', (err, origin) => {
6168
console.error(`${parseInt(Number(new Date()) / 1000)} # Serious problem (${origin}) - this should not happen but the JBNC is still running. ${err.stack}`);
69+
global.LAST_BUG = new Date().toLocaleString();
6270
});
6371
}
6472

lib/Connections.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ class Connections {
1919
}
2020
return channels;
2121
}
22+
23+
userKill(hash) {
24+
let disconnected = false;
25+
if (this.connections[hash]) {
26+
this.connections[hash].end();
27+
disconnected=true;
28+
}
29+
return disconnected;
30+
}
2231
}
2332

2433
module.exports = Connections;

lib/Server.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ const ClientConnect = require('../lib/ClientConnect');
88
const ClientReconnect = require('../lib/ClientReconnect');
99
const Connections = require('../lib/Connections');
1010

11+
var WebAdminPanel = null;
12+
if (global.WEBADMINPANEL)
13+
WebAdminPanel = require('../lib/WebAdminPanel');
14+
1115
class Server {
1216
constructor() {
1317
this.config = global.config;
@@ -747,6 +751,11 @@ class Server {
747751
if (global.DEBUG)
748752
console.log("The Bouncer Server is started. listen()");
749753

754+
// Web admin panel
755+
if (global.WEBADMINPANEL) {
756+
const webPanel = new WebAdminPanel(global.WEBADMINPANEL_PORT, this.connections, this.instanceConnections);
757+
webPanel.start();
758+
}
750759
}
751760

752761
}

lib/WebAdminPanel.js

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
const http = require('http');
2+
const express = require('express');
3+
const session = require('express-session');
4+
5+
class WebAdminPanel {
6+
constructor(port, connections, instanceConnections) {
7+
this.port = port;
8+
this.connections = connections;
9+
this.instanceConnections = instanceConnections;
10+
this.app = express();
11+
this.app.use(express.urlencoded({ extended: true }));
12+
this.app.use(express.json());
13+
this.setupMiddleware();
14+
this.setupRoutes();
15+
this.setup404Error();
16+
}
17+
18+
start() {
19+
this.server = http.createServer(this.app);
20+
this.server.listen(this.port, () => {
21+
console.log(`Server WebPanel started and listening on port http://localhost:${this.port}`);
22+
});
23+
}
24+
25+
setupMiddleware() {
26+
this.app.use(session({
27+
secret: global.WEBADMINPANEL_SECRET,
28+
resave: false,
29+
saveUninitialized: true
30+
}));
31+
}
32+
33+
setupRoutes() {
34+
this.app.get('/', (req, res) => {
35+
res.sendFile(__dirname + '/webadminpanel/login.html');
36+
});
37+
38+
this.app.post('/login', (req, res) => {
39+
const { username, password } = req.body;
40+
if (!!global.WEBADMINPANEL_SECRET && !!global.BOUNCER_ADMIN && !!global.WEBADMINPANEL_PASSWORD && username === global.BOUNCER_ADMIN && password === global.WEBADMINPANEL_PASSWORD) {
41+
req.session.authenticated = true;
42+
res.redirect('/dashboard');
43+
} else {
44+
res.status(401).send('Incorrect username or password');
45+
}
46+
});
47+
48+
this.app.get('/dashboard', (req, res) => {
49+
if (req.session.authenticated) {
50+
res.sendFile(__dirname + '/webadminpanel/dashboard.html');
51+
} else {
52+
res.redirect('/');
53+
}
54+
});
55+
56+
this.app.get('/connections.json', (req, res) => {
57+
if (req.session.authenticated) {
58+
let connectionsData = [];
59+
for (const key in this.connections) {
60+
if (Object.hasOwnProperty.call(this.connections, key)) {
61+
let connection = {
62+
key: key,
63+
nick: this.connections[key].nick,
64+
channelCount: this.instanceConnections.userChannelCount(key)
65+
};
66+
connectionsData.push(connection);
67+
}
68+
}
69+
let responseData = {
70+
connections: connectionsData,
71+
count: connectionsData.length,
72+
last_launch: global.LAST_LAUNCH,
73+
last_bug: global.LAST_BUG
74+
};
75+
76+
res.json(responseData);
77+
} else {
78+
res.redirect('/');
79+
}
80+
81+
});
82+
83+
this.app.get('/send', (req, res) => {
84+
const command = req.query.command;
85+
if (req.session.authenticated) {
86+
if (command === 'kill') {
87+
const key = req.query.key;
88+
let disconnected = this.instanceConnections.userKill(key);
89+
if(disconnected)
90+
res.send("disconnected");
91+
else
92+
res.send("none disconnected");
93+
}
94+
else {
95+
res.send("none");
96+
}
97+
} else {
98+
res.redirect('/');
99+
}
100+
101+
});
102+
}
103+
104+
setup404Error() {
105+
this.app.use((req, res, next) => {
106+
const errorMessage = "Sorry, the page you are looking for doesn't exist.";
107+
res.status(404).send(`
108+
<!DOCTYPE html>
109+
<html lang="en">
110+
<head>
111+
<meta charset="UTF-8">
112+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
113+
<title>Error 404</title>
114+
</head>
115+
<body>
116+
<h1>Error 404</h1>
117+
<p>${errorMessage}</p>
118+
</body>
119+
</html>
120+
`);
121+
});
122+
}
123+
}
124+
125+
126+
127+
module.exports = WebAdminPanel;

lib/webadminpanel/dashboard.html

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Jbnc's status</title>
7+
<script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
8+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
9+
<style>
10+
body {
11+
padding: 0 20px;
12+
}
13+
</style>
14+
</head>
15+
16+
<body>
17+
<div id="app">
18+
<h1>JBNC</h1>
19+
<h4 class="mt-3">Last Launch: {{ last_launch }}</h4>
20+
<h5>Last UncaughtException: {{ last_bug }}</h5>
21+
<h2 class="mt-4">Connections (total: {{ count }}) :</h2>
22+
<ul>
23+
<li v-for="connection in connections" :key="connection.id">
24+
Key: {{ connection.key }} - Nick: {{ connection.nick }} - Number of channels: {{ connection.channelCount }} - <button class="kill" :data-key="connection.key">Kill</button>
25+
</li>
26+
</ul>
27+
</div>
28+
29+
<script>
30+
const app = new Vue({
31+
el: '#app',
32+
data: {
33+
connections: [],
34+
count: 0,
35+
last_launch: 'Pending',
36+
last_bug: 'Pending'
37+
},
38+
methods: {
39+
fetchData() {
40+
const url = '/connections.json';
41+
fetch(url)
42+
.then(response => {
43+
if (!response.ok) {
44+
throw new Error('The request failed.');
45+
}
46+
return response.json();
47+
})
48+
.then(data => {
49+
this.connections = data.connections;
50+
this.count = data.count;
51+
this.last_launch = data.last_launch;
52+
this.last_bug = data.last_bug;
53+
})
54+
.catch(error => {
55+
console.error('Error while retrieving data:', error);
56+
});
57+
}
58+
},
59+
mounted() {
60+
this.fetchData();
61+
setInterval(() => {
62+
this.fetchData();
63+
}, 5000);
64+
}
65+
});
66+
67+
document.addEventListener("DOMContentLoaded", function() {
68+
document.addEventListener("click", function(event) {
69+
if (event.target.classList.contains('kill')) {
70+
var data = {
71+
command: "kill",
72+
key: event.target.getAttribute('data-key')
73+
};
74+
75+
var url = new URL("/send", window.location.origin);
76+
url.search = new URLSearchParams(data);
77+
78+
fetch(url.href)
79+
.then(function(response) {
80+
if (response.ok) {
81+
return response.text();
82+
} else {
83+
throw new Error('Error fetching data');
84+
}
85+
})
86+
.then(function(data) {
87+
if (data == "disconnected") {
88+
event.target.innerText="disconnected !";
89+
event.target.disabled=true;
90+
}
91+
else
92+
console.log("Error !");
93+
})
94+
.catch(function(error) {
95+
console.error("Error sending request:", error);
96+
});
97+
}
98+
});
99+
});
100+
101+
102+
103+
</script>
104+
</body>
105+
</html>

0 commit comments

Comments
 (0)