pointsight/main.ts

371 lines
13 KiB
TypeScript

/***
* 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)
})