[mod] logging [mod] code structure [mod] use hashed IP address as username
Christian Fraß

Christian Fraß commited on 2021-11-19 10:18:07
Zeige 7 geänderte Dateien mit 448 Einfügungen und 92 Löschungen.

... ...
@@ -0,0 +1,5 @@
1
+{
2
+	"port": 7979,
3
+	"verbosity": 1
4
+}
5
+
... ...
@@ -567,6 +567,57 @@ declare module lib_code {
567 567
         decode(x: type_flatten_to): type_flatten_from;
568 568
     }
569 569
 }
570
+declare module lib_json {
571
+    /**
572
+     * @author fenris
573
+     */
574
+    function encode(x: any, formatted?: boolean): string;
575
+    /**
576
+     * @author fenris
577
+     */
578
+    function decode(x: string): any;
579
+}
580
+declare module lib_json {
581
+    /**
582
+     * @author fenris
583
+     */
584
+    class class_json implements lib_code.interface_code<any, string> {
585
+        /**
586
+         * @author fenris
587
+         */
588
+        constructor();
589
+        /**
590
+         * @implementation
591
+         * @author fenris
592
+         */
593
+        encode(x: any): string;
594
+        /**
595
+         * @implementation
596
+         * @author fenris
597
+         */
598
+        decode(x: string): any;
599
+    }
600
+}
601
+declare namespace lib_sha256 {
602
+    /**
603
+     * @author fenris
604
+     */
605
+    function get(value: string, secret?: string): string;
606
+}
607
+declare namespace lib_plankton.file {
608
+    /**
609
+     * @author fenris
610
+     */
611
+    function read(path: string): Promise<string>;
612
+    /**
613
+     * @author fenris
614
+     */
615
+    function read_stdin(): Promise<string>;
616
+    /**
617
+     * @author fenris
618
+     */
619
+    function write(path: string, content: string): Promise<void>;
620
+}
570 621
 declare module lib_http {
571 622
     /**
572 623
      * @author fenris <frass@greenscale.de>
... ...
@@ -658,19 +709,25 @@ declare module lib_http {
658 709
     }
659 710
 }
660 711
 declare module lib_server {
712
+    /**
713
+     * @author fenris
714
+     */
715
+    type type_metadata = {
716
+        ip_address: string;
717
+    };
661 718
     /**
662 719
      * @author fenris
663 720
      */
664 721
     type type_subject = {
665 722
         port: int;
666
-        handle: (input: string) => Promise<string>;
723
+        handle: (input: string, metadata?: type_metadata) => Promise<string>;
667 724
         verbosity: int;
668 725
         serverobj: any;
669 726
     };
670 727
     /**
671 728
      * @author fenris
672 729
      */
673
-    function make(port: int, handle: (input: string) => Promise<string>, verbosity?: int): type_subject;
730
+    function make(port: int, handle: (input: string, metadata?: type_metadata) => Promise<string>, verbosity?: int): type_subject;
674 731
     /**
675 732
      * @author fenris
676 733
      */
... ...
@@ -1185,6 +1185,205 @@ var lib_code;
1185 1185
     lib_code.class_code_flatten = class_code_flatten;
1186 1186
 })(lib_code || (lib_code = {}));
1187 1187
 /*
1188
+This file is part of »bacterio-plankton:json«.
1189
+
1190
+Copyright 2016-2021 'Christian Fraß, Christian Neubauer, Martin Springwald GbR'
1191
+<info@greenscale.de>
1192
+
1193
+»bacterio-plankton:json« is free software: you can redistribute it and/or modify
1194
+it under the terms of the GNU Lesser General Public License as published by
1195
+the Free Software Foundation, either version 3 of the License, or
1196
+(at your option) any later version.
1197
+
1198
+»bacterio-plankton:json« is distributed in the hope that it will be useful,
1199
+but WITHOUT ANY WARRANTY; without even the implied warranty of
1200
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1201
+GNU Lesser General Public License for more details.
1202
+
1203
+You should have received a copy of the GNU Lesser General Public License
1204
+along with »bacterio-plankton:json«. If not, see <http://www.gnu.org/licenses/>.
1205
+ */
1206
+var lib_json;
1207
+(function (lib_json) {
1208
+    /**
1209
+     * @author fenris
1210
+     */
1211
+    function encode(x, formatted = false) {
1212
+        return JSON.stringify(x, undefined, formatted ? "\t" : undefined);
1213
+    }
1214
+    lib_json.encode = encode;
1215
+    /**
1216
+     * @author fenris
1217
+     */
1218
+    function decode(x) {
1219
+        return JSON.parse(x);
1220
+    }
1221
+    lib_json.decode = decode;
1222
+})(lib_json || (lib_json = {}));
1223
+/*
1224
+This file is part of »bacterio-plankton:json«.
1225
+
1226
+Copyright 2016-2021 'Christian Fraß, Christian Neubauer, Martin Springwald GbR'
1227
+<info@greenscale.de>
1228
+
1229
+»bacterio-plankton:json« is free software: you can redistribute it and/or modify
1230
+it under the terms of the GNU Lesser General Public License as published by
1231
+the Free Software Foundation, either version 3 of the License, or
1232
+(at your option) any later version.
1233
+
1234
+»bacterio-plankton:json« is distributed in the hope that it will be useful,
1235
+but WITHOUT ANY WARRANTY; without even the implied warranty of
1236
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1237
+GNU Lesser General Public License for more details.
1238
+
1239
+You should have received a copy of the GNU Lesser General Public License
1240
+along with »bacterio-plankton:json«. If not, see <http://www.gnu.org/licenses/>.
1241
+ */
1242
+var lib_json;
1243
+(function (lib_json) {
1244
+    /**
1245
+     * @author fenris
1246
+     */
1247
+    class class_json {
1248
+        /**
1249
+         * @author fenris
1250
+         */
1251
+        constructor() {
1252
+        }
1253
+        /**
1254
+         * @implementation
1255
+         * @author fenris
1256
+         */
1257
+        encode(x) {
1258
+            return lib_json.encode(x);
1259
+        }
1260
+        /**
1261
+         * @implementation
1262
+         * @author fenris
1263
+         */
1264
+        decode(x) {
1265
+            return lib_json.decode(x);
1266
+        }
1267
+    }
1268
+    lib_json.class_json = class_json;
1269
+})(lib_json || (lib_json = {}));
1270
+/*
1271
+This file is part of »bacterio-plankton:sha256«.
1272
+
1273
+Copyright 2016-2021 'Christian Fraß, Christian Neubauer, Martin Springwald GbR'
1274
+<info@greenscale.de>
1275
+
1276
+»bacterio-plankton:sha256« is free software: you can redistribute it and/or modify
1277
+it under the terms of the GNU Lesser General Public License as published by
1278
+the Free Software Foundation, either version 3 of the License, or
1279
+(at your option) any later version.
1280
+
1281
+»bacterio-plankton:sha256« is distributed in the hope that it will be useful,
1282
+but WITHOUT ANY WARRANTY; without even the implied warranty of
1283
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1284
+GNU Lesser General Public License for more details.
1285
+
1286
+You should have received a copy of the GNU Lesser General Public License
1287
+along with »bacterio-plankton:sha256«. If not, see <http://www.gnu.org/licenses/>.
1288
+ */
1289
+var lib_sha256;
1290
+(function (lib_sha256) {
1291
+    /**
1292
+     * @author fenris
1293
+     */
1294
+    function get(value, secret = "") {
1295
+        const nm_crypto = require("crypto");
1296
+        const sha256Hasher = nm_crypto.createHmac("sha256", secret);
1297
+        const hash = sha256Hasher.update(value).digest("hex");
1298
+        return hash;
1299
+    }
1300
+    lib_sha256.get = get;
1301
+})(lib_sha256 || (lib_sha256 = {}));
1302
+/*
1303
+This file is part of »bacterio-plankton:file«.
1304
+
1305
+Copyright 2016-2021 'Christian Fraß, Christian Neubauer, Martin Springwald GbR'
1306
+<info@greenscale.de>
1307
+
1308
+»bacterio-plankton:file« is free software: you can redistribute it and/or modify
1309
+it under the terms of the GNU Lesser General Public License as published by
1310
+the Free Software Foundation, either version 3 of the License, or
1311
+(at your option) any later version.
1312
+
1313
+»bacterio-plankton:file« is distributed in the hope that it will be useful,
1314
+but WITHOUT ANY WARRANTY; without even the implied warranty of
1315
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1316
+GNU Lesser General Public License for more details.
1317
+
1318
+You should have received a copy of the GNU Lesser General Public License
1319
+along with »bacterio-plankton:file«. If not, see <http://www.gnu.org/licenses/>.
1320
+ */
1321
+var lib_plankton;
1322
+(function (lib_plankton) {
1323
+    var file;
1324
+    (function (file) {
1325
+        /**
1326
+         * @author fenris
1327
+         */
1328
+        function read(path) {
1329
+            var nm_fs = require("fs");
1330
+            return (new Promise(function (resolve, reject) {
1331
+                nm_fs.readFile(path, {
1332
+                    "encoding": "utf8",
1333
+                    "flag": "r"
1334
+                }, function (error, content) {
1335
+                    if (error == null) {
1336
+                        resolve(content);
1337
+                    }
1338
+                    else {
1339
+                        reject(error);
1340
+                    }
1341
+                });
1342
+            }));
1343
+        }
1344
+        file.read = read;
1345
+        /**
1346
+         * @author fenris
1347
+         */
1348
+        function read_stdin() {
1349
+            return (new Promise(function (resolve, reject) {
1350
+                var input_raw = "";
1351
+                process.stdin.setEncoding("utf8");
1352
+                process.stdin.on("readable", function () {
1353
+                    var chunk;
1354
+                    while ((chunk = process.stdin.read()) !== null) {
1355
+                        input_raw += chunk;
1356
+                    }
1357
+                });
1358
+                process.stdin.on("end", function () {
1359
+                    resolve(input_raw);
1360
+                });
1361
+            }));
1362
+        }
1363
+        file.read_stdin = read_stdin;
1364
+        /**
1365
+         * @author fenris
1366
+         */
1367
+        function write(path, content) {
1368
+            var nm_fs = require("fs");
1369
+            return (new Promise(function (resolve, reject) {
1370
+                nm_fs.writeFile(path, content, {
1371
+                    "encoding": "utf8",
1372
+                    "flag": "w"
1373
+                }, function (error) {
1374
+                    if (error == null) {
1375
+                        resolve(undefined);
1376
+                    }
1377
+                    else {
1378
+                        reject(error);
1379
+                    }
1380
+                });
1381
+            }));
1382
+        }
1383
+        file.write = write;
1384
+    })(file = lib_plankton.file || (lib_plankton.file = {}));
1385
+})(lib_plankton || (lib_plankton = {}));
1386
+/*
1188 1387
 This file is part of »bacterio-plankton:http«.
1189 1388
 
1190 1389
 Copyright 2016-2021 'Christian Fraß, Christian Neubauer, Martin Springwald GbR'
... ...
@@ -1542,7 +1741,10 @@ var lib_server;
1542 1741
                     while (!((chunk = socket.read()) === null)) {
1543 1742
                         const input = chunk.toString();
1544 1743
                         log(subject, 3, "reading: " + input);
1545
-                        subject.handle(input)
1744
+                        const metadata = {
1745
+                            "ip_address": socket.remoteAddress,
1746
+                        };
1747
+                        subject.handle(input, metadata)
1546 1748
                             .then((output) => {
1547 1749
                             log(subject, 3, "writing: " + output);
1548 1750
                             socket.write(output);
... ...
@@ -13,10 +13,11 @@ A simple mediator between an IRC server and a web application
13 13
 
14 14
 ## Procedure
15 15
 
16
-- run `tools/build`
16
+- create a configuration file in the `conf` directory (e.g. `foobar.json`); deriving from `conf/example.json` might be a good idea
17
+- run `tools/build [<conf-profile>]` (e.g. `tools/build foobar`)
17 18
 
18 19
 
19 20
 # Running
20 21
 
21
-After building just execute `build/irc-web-backend`!
22
+After building just execute `cd build && ./irc-web-backend`!
22 23
 
... ...
@@ -1,96 +1,125 @@
1
-type type_event = {
1
+type type_event =
2
+{
2 3
 	timestamp: int;
3 4
 	kind: string;
4 5
 	data: any;
5 6
 };
6 7
 
7 8
 
8
-type type_user = {
9
+type type_user =
10
+{
9 11
 	name: string;
10 12
 	role: string;
11 13
 };
12 14
 
13 15
 
14
-type type_connection = {
16
+type type_connection =
17
+{
15 18
 	events: Array<type_event>;
16 19
 	users: Array<type_user>;
17 20
 	client: any;
18 21
 };
19 22
 
20 23
 
21
-function get_timestamp(): int
24
+type type_internal_request =
22 25
 {
23
-	return Math.floor(Date.now()/1000);
24
-}
26
+	id: (null | string);
27
+	action: string;
28
+	data: any;
29
+};
25 30
 
26 31
 
27
-function log(message: string): void
32
+type type_internal_response = any;
33
+
34
+
35
+function get_timestamp(): int
28 36
 {
29
-	process.stderr.write(`-- ${message}\n`);
37
+	return Math.floor(Date.now()/1000);
30 38
 }
31 39
 
32
-
33 40
 function generate_id(): string
34 41
 {
35 42
 	return (Math.random() * (1 << 24)).toFixed(0).padStart(8, '0');
36 43
 }
37 44
 
38 45
 
39
-var connections: Record<string, type_connection> = {};
46
+var nm_irc: any = require("irc");
47
+
48
+
49
+var _connections: Record<string, type_connection> = {};
40 50
 
41 51
 
42
-function get_connection(data_in: any): type_connection
52
+var _conf: any = {};
53
+
54
+
55
+function log(level: int, incident: string, details: Record<string, any> = {}): void
43 56
 {
44
-	if (! connections.hasOwnProperty(data_in["id"])) {
45
-		throw (new Error("no connection for ID '" + data_in["id"] + "'"));
46
-	}
47
-	else {
48
-		return connections[data_in["id"]];
57
+	if (level <= _conf["verbosity"])
58
+	{
59
+		process.stderr.write(`-- ${incident} | ${lib_json.encode(details)}\n`);
49 60
 	}
50 61
 }
51 62
 
52 63
 
53
-async function main(): Promise<void>
64
+function get_connection(id: string): type_connection
54 65
 {
55
-	var nm_irc: any = require("irc");
66
+	if (! _connections.hasOwnProperty(id))
67
+	{
68
+		throw (new Error("no connection for ID '" + id + "'"));
69
+	}
70
+	else
71
+	{
72
+		return _connections[id];
73
+	}
74
+}
56 75
 
57
-	const server: lib_server.class_server = new lib_server.class_server(
58
-		7979,
59
-		(input: string) => {
60
-			const request: lib_http.type_request = lib_http.decode_request(input);
61
-			// process.stderr.write(JSON.stringify(request, undefined, "\t") + "\n");
62
-			const data_in: any = JSON.parse(request.body);
63
-			// log(data_in["action"] + " | " + (data_in["id"] ?? "-") + " | " + JSON.stringify(data_in["data"]));
64
-			let data_out: any;
65
-			let error: (null | Error);
66
-			try {
67
-				switch (data_in["action"]) {
68
-					case "connect": {
69
-						if (data_in.hasOwnProperty("id") && connections.hasOwnProperty(data_in["id"])) {
76
+
77
+async function execute(internal_request: type_internal_request, ip_address: string): Promise<type_internal_response>
78
+{
79
+	switch (internal_request.action)
80
+	{
81
+		default:
82
+		{
83
+			throw (new Error("unhandled action: " + internal_request.action));
84
+			break;
85
+		}
86
+		case "connect":
87
+		{
88
+			if (_connections.hasOwnProperty(internal_request.id))
89
+			{
70 90
 				throw (new Error("already connected"));
71 91
 			}
72
-						else {
92
+			else
93
+			{
73 94
 				const id: string = generate_id();
74
-							const client = new nm_irc.Client(
75
-								data_in["data"]["server"],
76
-								data_in["data"]["nickname"],
95
+				const client = new nm_irc.Client
96
+				(
97
+					internal_request.data["server"],
98
+					internal_request.data["nickname"],
77 99
 					{
78
-									"channels": data_in["data"]["channels"],
100
+						"userName": lib_sha256.get(ip_address).slice(0, 8),
101
+						"channels": internal_request.data["channels"],
102
+						"showErrors": true,
79 103
 						"autoConnect": false,
80 104
 					}
81 105
 				);
82
-							const connection: type_connection = {
106
+				const connection: type_connection = 
107
+				{
83 108
 					"client": client,
84 109
 					"events": [],
85 110
 					"users": [],
86 111
 				};
87
-							client.addListener(
112
+				client.addListener
113
+				(
88 114
 					"message",
89
-								(from, to, message) => {
90
-									connection.events.push({
115
+					(from, to, message) =>
116
+					{
117
+						connection.events.push
118
+						({
91 119
 							"timestamp": get_timestamp(),
92 120
 							"kind": "channel_message",
93
-										"data": {
121
+							"data":
122
+							{
94 123
 								"from": from,
95 124
 								"to": to,
96 125
 								"message": message,
... ...
@@ -98,105 +127,144 @@ async function main(): Promise<void>
98 127
 						});
99 128
 					}
100 129
 				);
101
-							client.addListener(
130
+				client.addListener
131
+				(
102 132
 					"pm",
103
-								(from, message) => {
104
-									connection.events.push({
133
+					(from, message) =>
134
+					{
135
+						connection.events.push
136
+						({
105 137
 							"timestamp": get_timestamp(),
106 138
 							"kind": "private_message",
107
-										"data": {
139
+							"data":
140
+							{
108 141
 								"from": from,
109 142
 								"message": message,
110 143
 							}
111 144
 						});
112 145
 					}
113 146
 				);
114
-							client.addListener(
147
+				client.addListener
148
+				(
115 149
 					"names",
116
-								(channel, users) => {
150
+					(channel, users) =>
151
+					{
117 152
 						connection.users = Object.entries(users).map(([key, value]) => ({"name": key, "role": value.toString()}));
118 153
 					}
119 154
 				);
120
-							client.addListener(
155
+				client.addListener
156
+				(
121 157
 					"error",
122
-								(error) => {
123
-									log("error: " + error.message);
158
+					(error) =>
159
+					{
160
+						log(0, "irc_error", {"reason": error.message});
124 161
 					}
125 162
 				);
126
-							client.connect(
163
+				client.connect
164
+				(
127 165
 					3,
128
-								() => {
129
-									connections[id] = connection;
166
+					() =>
167
+					{
168
+						_connections[id] = connection;
130 169
 					}
131 170
 				);
132
-							data_out = id;
171
+				return Promise.resolve<type_internal_response>(id);
133 172
 			}
134 173
 			break;
135 174
 		}
136
-					case "check": {
137
-						try {
138
-							get_connection(data_in);
139
-							data_out = true;
175
+		case "check":
176
+		{
177
+			try
178
+			{
179
+				get_connection(internal_request.id);
180
+				return Promise.resolve<type_internal_response>(true);
140 181
 			}
141
-						catch (error) {
142
-							data_out = false;
182
+			catch (error)
183
+			{
184
+				return Promise.resolve<type_internal_response>(false);
143 185
 			}
144 186
 			break;
145 187
 		}
146
-					case "disconnect": {
147
-						const connection: type_connection = get_connection(data_in);
148
-						delete connections[data_in["id"]];
188
+		case "disconnect":
189
+		{
190
+			const connection: type_connection = get_connection(internal_request.id);
191
+			delete _connections[internal_request.id];
149 192
 			connection.client.disconnect("", () => {});
150
-						data_out = null;
193
+			return Promise.resolve<type_internal_response>(null);
151 194
 			break;
152 195
 		}
153
-					case "say": {
154
-						const connection: type_connection = get_connection(data_in);
155
-						connection.client.say(data_in["data"]["channel"], data_in["data"]["message"]);
156
-						data_out = null;
196
+		case "say":
197
+		{
198
+			const connection: type_connection = get_connection(internal_request.id);
199
+			connection.client.say(internal_request.data["channel"], internal_request.data["message"]);
200
+			return Promise.resolve<type_internal_response>(null);
157 201
 			break;
158 202
 		}
159
-					case "fetch": {
160
-						const connection: type_connection = get_connection(data_in);
161
-						data_out = {
203
+		case "fetch":
204
+		{
205
+			const connection: type_connection = get_connection(internal_request.id);
206
+			const internal_response: type_internal_response =
207
+			{
162 208
 				"users": connection.users,
163 209
 				"events": connection.events,
164 210
 			};
165 211
 			connection.events = [];
212
+			return Promise.resolve<type_internal_response>(internal_response);
166 213
 			break;
167 214
 		}
168 215
 	}
216
+}
217
+
218
+async function main(): Promise<void>
219
+{
220
+	_conf = await lib_plankton.file.read("conf.json").then<any>(x => lib_json.decode(x));
221
+	const server: lib_server.class_server = new lib_server.class_server
222
+	(
223
+		_conf["port"],
224
+		async (input: string, metadata?: lib_server.type_metadata): Promise<string> =>
225
+		{
226
+			const http_request: lib_http.type_request = lib_http.decode_request(input);
227
+			log(2, "http_request", http_request);
228
+			const internal_request: type_internal_request = lib_json.decode(http_request.body);
229
+			log(1, "internal_request", internal_request);
230
+			let internal_response: type_internal_response;
231
+			let error: (null | Error);
232
+			try
233
+			{
234
+				internal_response = await execute(internal_request, metadata.ip_address);
169 235
 				error = null;
170 236
 			}
171
-			catch (error_) {
172
-				process.stderr.write(error_.toString() + "\n");
237
+			catch (error_)
238
+			{
239
+				internal_response = null;
173 240
 				error = error_;
174 241
 			}
175
-			const response: lib_http.type_response = (
242
+			if (error !== null)
243
+			{
244
+				log(0, "error_in_execution", {"reason": error.toString()});
245
+			}
246
+			const http_response: lib_http.type_response =
247
+			(
176 248
 				(error !== null)
177 249
 				? {
178 250
 					"statuscode": 500,
179
-					"headers": {
180
-						"Content-Type": "text/plain",
181
-						"Access-Control-Allow-Origin": "*",
182
-					},
183
-					"body": error.toString(),
251
+					"headers": {"Access-Control-Allow-Origin": "*", "Content-Type": "text/plain"},
252
+					"body": "error executing the request; check the server logs for details",
184 253
 				}
185 254
 				: {
186 255
 					"statuscode": 200,
187
-					"headers": {
188
-						"Content-Type": "application/json",
189
-						"Access-Control-Allow-Origin": "*",
190
-					},
191
-					"body": JSON.stringify(data_out),
256
+					"headers": {"Access-Control-Allow-Origin": "*", "Content-Type": "application/json"},
257
+					"body": lib_json.encode(internal_response)
192 258
 				}
193 259
 			);
194
-			const output: string = lib_http.encode_response(response);
260
+			log(2, "http_response", http_response);
261
+			const output: string = lib_http.encode_response(http_response);
195 262
 			return Promise.resolve<string>(output);
196 263
 		}
197 264
 	);
198
-	server.start()
265
+	return server.start();
199 266
 }
200 267
 
268
+
201 269
 main();
202 270
 
... ...
@@ -1,4 +1,27 @@
1 1
 #!/usr/bin/env sh
2 2
 
3
+## consts
4
+
5
+dir_build="build"
6
+dir_conf="conf"
7
+
8
+
9
+## args
10
+
11
+if [ $# -ge 1 ] ; then profile=$1 ; else profile="example" ; fi
12
+
13
+
14
+## exec
15
+
16
+echo ">> building …"
3 17
 make -f tools/makefile
4 18
 
19
+if $(test ${profile} = '-')
20
+then
21
+	# do nothing
22
+	echo ">> no profile given; not placing configuration file"
23
+else
24
+	echo ">> placing configuration for profile '${profile}' …"
25
+	cp -ru ${dir_conf}/${profile}.json ${dir_build}/conf.json
26
+fi	
27
+
... ...
@@ -2,5 +2,5 @@
2 2
 
3 3
 dir_lib="lib"
4 4
 cd ${dir_lib}/plankton
5
-ptk bundle node http server
5
+ptk bundle node json sha256 file http server
6 6
 
7 7