/*** * Copyright © 2021 Sören W. Oesterwind (TheGreydiamond). All rights reserved. */ // Requires (Imports) const express = require("express"); const fs = require("fs"); const mysql = require('mysql'); const autobahnAPI = require("./apiHandler/autobahn/main.js"); /*const ffhAPI = require("./apiHandler/ffh/main.js"); const stadtkoelnAPI = require("./apiHandler/ffh/main.js");*/ const customWebcamAPI = require("./apiHandler/customWebcams/main.js"); const airspyAPI = require("./apiHandler/airspydirectory/main"); const ttncommsAPI = require("./apiHandler/ttncomms/main"); const freifunkAPI = require("./apiHandler/freifunk/main"); const senseboxAPI = require("./apiHandler/opensensemap/main.js"); const opentopiaAPI = require("./apiHandler/opentopia/main.js"); const ttnGateway = require("./apiHandler/ttnantennas/main"); const stubMod = require("./apiHandler/stub/main"); const _ = require("underscore"); const chalk = require('chalk'); const func = require("./functions.js") const minify = require('express-minify'); const compression = require('compression'); const cookieParser = require('cookie-parser') const bodyParser = require('body-parser'); const Sentry = require('@sentry/node'); const Tracing = require('@sentry/tracing'); const bent = require("bent"); const Influx = require("influx"); function padLeadingZeros(num, size) { let s = num + ""; while (s.length < size) s = "0" + s; return s; } const enabledModules = [airspyAPI, autobahnAPI, customWebcamAPI, freifunkAPI, senseboxAPI, opentopiaAPI, ttnGateway, ttncommsAPI]; // Webserver init const app = express(); const allTags = {}; const metaGlobals = { "desc": "Pointsight is a centralized geolocalized API aggragator.", "titlePrefx": "Pointsight - " } // Skeleton Variables let jsonConfigGlobal = { fontAwesome: undefined, mapboxAccessToken: undefined, cookieSecret: undefined, mapquest: undefined, here: undefined, sentryDsn: undefined, database: { host: "127.0.0.1", user: "pointsight", password: "", database: "pointsight", customSettings: { wait_timout: 28800 } }, env: "PROD", maint: true, betaMode: false, port: 3000, adress: '127.0.0.1', taxonomyCacheInterval: 5, metrics: { influx: { enable: false, host: "127.0.0.1", database: "pointsight", user: "pointsight", password: "" }, writeMetricsToMySQL: true } } // Load config try { const data = fs.readFileSync("config/default.json", "utf8"); jsonConfigGlobal = _.extend(jsonConfigGlobal, JSON.parse(data)); } catch (error) { console.error( "While reading the config an error occured. The error was: " + error ); } console.log(jsonConfigGlobal.metrics.influx) Sentry.init({ dsn: jsonConfigGlobal.sentryDsn, integrations: [ // enable HTTP calls tracing new Sentry.Integrations.Http({ tracing: true }), // enable Express.js middleware tracing new Tracing.Integrations.Express({ app }), new Tracing.Integrations.Mysql({ useMysql: true }) ], // Set tracesSampleRate to 1.0 to capture 100% // of transactions for performance monitoring. // We recommend adjusting this value in production tracesSampleRate: 0.8, }); app.use(Sentry.Handlers.requestHandler()); app.use(Sentry.Handlers.tracingHandler()); const con = mysql.createConnection(jsonConfigGlobal.database); const connectionPool = mysql.createPool(jsonConfigGlobal.database); const allPoolConnections = []; let influxD = undefined; let isInfluxReady = false; let lastHttpRequest = Math.floor(Date.now() / 1000) if(jsonConfigGlobal.metrics.influx.enable){ console.log("[Influx] Connecting to InfluxDB..."); influxD = new Influx.InfluxDB({ host: jsonConfigGlobal.metrics.influx.host, database: jsonConfigGlobal.metrics.influx.database, user: jsonConfigGlobal.metrics.influx.user, password: jsonConfigGlobal.metrics.influx.password, schema: [ { measurement: 'apitaxonomy', fields: { calls: Influx.FieldType.INTEGER }, tags: [] } ] }); console.log("[Influx] Checking if database exists..."); influxD.getDatabaseNames() .then(names => { if (!names.includes('pointsight')) { return influxD.createDatabase('pointsight'); } }) .then(() => { console.log("[Influx] Database ready."); isInfluxReady = true; }) .catch(error => console.log({ error })); } connectionPool.getConnection((err, conn) => { conn.on('error', function (err) { console.log("----[MYSQL ERROR]----") console.log(err); // 'ER_BAD_DB_ERROR' console.log("[MYSQL ERROR] Connection pool error"); console.log("----]MYSQL ERROR[----") }); func.handleMysqlErrors(err) //Create system tabels const queries = ["CREATE TABLE IF NOT EXISTS `apikeys` ( `id` BIGINT NOT NULL AUTO_INCREMENT , `apikey` VARCHAR(64) NOT NULL , `owner` VARCHAR(64) NOT NULL , `hosts` JSON NOT NULL , `created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP , `expire` DATETIME NOT NULL , PRIMARY KEY (`id`));", "CREATE TABLE IF NOT EXISTS `betatokens` ( `id` BIGINT NOT NULL AUTO_INCREMENT , `token` VARCHAR(64) NOT NULL , `owner` VARCHAR(64) NOT NULL, `created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP , `expire` DATETIME NOT NULL , PRIMARY KEY (`id`));", "CREATE TABLE IF NOT EXISTS `reports` ( `id` INT NOT NULL AUTO_INCREMENT , `point_id` VARCHAR(512) NOT NULL , `mail` VARCHAR(512) NOT NULL , `comment` VARCHAR(2048) NOT NULL , PRIMARY KEY (`id`)) ENGINE = InnoDB;", "CREATE TABLE IF NOT EXISTS `apitaxonomy` ( `id` INT NOT NULL AUTO_INCREMENT , `apiKey` VARCHAR(255) NOT NULL , `date` DATE NOT NULL DEFAULT CURRENT_TIMESTAMP , `calls` INT NOT NULL DEFAULT '0' , PRIMARY KEY (`id`)) ENGINE = InnoDB;"] queries.forEach(element => { conn.query(element, function (err) { if (err) throw err; console.log("Table created for Main system"); }); }); conn.release() }) for (let i = 0; i < enabledModules.length; i++) { connectionPool.getConnection((err, conn) => { if (err) { if (err.code == "ECONNREFUSED") { console.log(chalk.red("⚠ Connection to database failed. Is the database server running? ⚠")) server.close() process.exit(5) } else { throw err; } } allPoolConnections.push(conn); conn.on('error', function (err) { console.log("----[MYSQL ERROR]----") console.log("[MYSQL ERROR] Module connection error"); console.log(err); // 'ER_BAD_DB_ERROR' if (err.code == "ECONNRESET") { connectionPool.getConnection((err, connP) => { if (err) throw err; conn = connP // Trying to mitigate ECONNRESET issues, even tho this didn't fix it lol }) } console.log("----]MYSQL ERROR[----") }); _.each(allPoolConnections, function (arg) { setInterval(function () { arg.query("SELECT 1") }, jsonConfigGlobal.database.customSettings.wait_timout - 50) }) try { enabledModules[i].initialize(conn, jsonConfigGlobal) const currTags = enabledModules[i].getModuleMeta().tags for (const tagy in currTags) { const tag = currTags[tagy] if (_.isFinite(allTags[tag])) { allTags[tag] = allTags[tag] + 1; } else { allTags[tag] = 1; } } } catch (error) { console.warn(chalk.red("⚠ Module " + enabledModules[i].getModuleMeta().title + " failed to initialize() ⚠" + "\n Error: " + error + "\n Stacktrace: " + error.stack)) enabledModules[i] = stubMod; } }); setInterval(function () { for (const conyT in allPoolConnections) { allPoolConnections[conyT].query("SELECT 1;", function (err) { if (err) throw err; console.log("Querrying heartbeat for connection pool number: " + conyT); }); } }, jsonConfigGlobal.database.customSettings.wait_timout * 1000) } app.use(express.static("static")); // A crappy logging middleware, please don't look at this app.use(function (req, res, next) { lastHttpRequest = Math.floor(Date.now() / 1000) const date_ob = new Date(); const date = ("0" + date_ob.getDate()).slice(-2); // current month const month = ("0" + (date_ob.getMonth() + 1)).slice(-2); // current year const year = date_ob.getFullYear(); // current hours const hours = padLeadingZeros(date_ob.getHours(), 2) // current minutes const minutes = padLeadingZeros(date_ob.getMinutes(), 2) // current seconds const seconds = padLeadingZeros(date_ob.getSeconds(), 2) // prints date & time in YYYY-MM-DD HH:MM:SS format console.log("[" + year + "-" + month + "-" + date + " " + hours + ":" + minutes + ":" + seconds + "] " + req.method + " " + req.path); next(); }); app.use(bodyParser.urlencoded({ extended: true })); app.use(cookieParser(jsonConfigGlobal.cookieSecret)); if (jsonConfigGlobal.betaMode) { require('./middleware/beta.middleware')(app, con, jsonConfigGlobal); } // API Taxonomy const apiTaxonomyCache = {}; function flushApiTaxonomyCache():void { if(jsonConfigGlobal.metrics.writeMetricsToMySQL){ const updateSql = "UPDATE apitaxonomy SET calls = calls+? WHERE apikey LIKE ? AND DATE(date) = CURDATE()"; const insertSql = "INSERT INTO apitaxonomy (apikey, calls) VALUES(?, ?);" console.log( Object.entries(apiTaxonomyCache).length + " entries in apiTaxonomyCache") for (const [key, value] of Object.entries(apiTaxonomyCache)) { con.query(updateSql, [value, key], function (err, result) { if (err) { console.error(err); } if(!jsonConfigGlobal.metrics.influx.enable){ apiTaxonomyCache[key] = 0; } if (result.affectedRows == 0) { con.query(insertSql, [key, value], function (err, result) { if (err) { console.error(err); } }); } }); } }else{ if(!jsonConfigGlobal.metrics.influx.enable){ console.log(chalk.red("⚠ MySQL Metrics writing has been disabled, but Influx is not setup! ⚠"));} } if(jsonConfigGlobal.metrics.influx.enable){ const toWritePoints = []; console.log( Object.entries(apiTaxonomyCache).length + " entries in apiTaxonomyCache in Influx") for (const [key, value] of Object.entries(apiTaxonomyCache)) { const val = apiTaxonomyCache[key] toWritePoints.push({ measurement: 'apitaxonomy', tags: {}, fields: { calls: val }, timestamp: new Date, }) apiTaxonomyCache[key] = 0; } influxD.writePoints(toWritePoints, { database: jsonConfigGlobal.metrics.influx.database, precision: 's', }) .catch(error => { console.error(`[InfluxDB] Error saving data to InfluxDB! ${error.stack}`) }); } } if(jsonConfigGlobal.env == "DEV"){ jsonConfigGlobal.taxonomyCacheInterval = 0.5; } setInterval(function () { flushApiTaxonomyCache() console.log("[API TAXONOMY] Wrote taxonomy to database"); }, jsonConfigGlobal.taxonomyCacheInterval * 60 * 1000); // Every `taxonomyCacheInterval` minutes connectionPool.getConnection((err, conn) => { conn.on('error', function (err) { console.log("----[MYSQL ERROR]----") console.log("[MYSQL ERROR] System connection error"); console.log(err); console.log("----]MYSQL ERROR[----") }); setInterval(function () { conn.query("SELECT 1;", function (err) { if (err) throw err; console.log("Querrying heartbeat for system connection"); }); }, jsonConfigGlobal.database.customSettings.wait_timout * 1000) // Every 20 Minutes func.handleMysqlErrors(err) require('./middleware/maint.middleware')(app, jsonConfigGlobal, metaGlobals); require('./middleware/apitoken.middleware')(app, con, apiTaxonomyCache); require('./routes/api.route.ts')(app, enabledModules, conn, { tags: allTags }); require('./routes/index.route.js')(app, metaGlobals, jsonConfigGlobal.fontAwesome, jsonConfigGlobal.mapboxAccessToken, jsonConfigGlobal); app.use(Sentry.Handlers.errorHandler()); require('./routes/error.route.js')(app, jsonConfigGlobal, metaGlobals); // Make sure this is always last app.use(minify()); app.use(compression()); }) let server = undefined; setInterval(function () { if (Math.floor(Date.now() / 1000) - lastHttpRequest >= 60 * 60) { // One hour after the last http request const request = bent("https://pointsight.project-name-here.de/api/getPOI?boundingEast=55.21649013168979;10.696563720703127&boundingWest=54.59354980818523;9.825897216796877&key=b03f8aaf-1f32-4d9e-914a-9a50f904833d", "GET", "json", 200); request().then(function (body) { console.log("Tried to pull POI from pointsight.project-name-here.de") }) } }, 60000); process.on('SIGINT', function () { console.log("Caught interrupt signal and shutting down gracefully"); flushApiTaxonomyCache(); // Save the api taxonomy cache before quitting server.close(); // Make th express server stop _.each(allPoolConnections, function (arg) { arg.release(); }) // Let go off all mysql pool connections con.end(); // Close the system's mysql connection Sentry.close(2000).then(function () { // Wait for sentry to quit console.log(chalk.blue("👋 Goodbye! 👋")) process.exit(); // Quit the application }); }); // Start server server = app.listen(jsonConfigGlobal.port, jsonConfigGlobal.adress, () => { console.log(`The Pointsight is running at http://localhost:${jsonConfigGlobal.port}`) setTimeout(function () { if (jsonConfigGlobal.env == "DEV") { console.log(allTags); } }, 1000) })