summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMinteck <contact@minteck.org>2022-11-28 17:14:38 +0100
committerMinteck <contact@minteck.org>2022-11-28 17:14:38 +0100
commit18efd30a263ec0d79a26a82cbd8c90c9f81056b7 (patch)
treeaea01bf3506dda706719fc68eb37b77ed9ef3fe8 /src
downloadautoreport-mane.tar.gz
autoreport-mane.tar.bz2
autoreport-mane.zip
Open sourceHEADmane
Diffstat (limited to 'src')
-rw-r--r--src/core/API.ts216
-rw-r--r--src/core/Authentication.ts93
-rw-r--r--src/core/Autoreport.ts74
-rw-r--r--src/core/AutoreportBase.ts10
-rw-r--r--src/core/Notification.ts41
-rw-r--r--src/index.ts32
-rw-r--r--src/types/Report.ts68
-rw-r--r--src/types/UUID.ts7
8 files changed, 541 insertions, 0 deletions
diff --git a/src/core/API.ts b/src/core/API.ts
new file mode 100644
index 0000000..bb0fab3
--- /dev/null
+++ b/src/core/API.ts
@@ -0,0 +1,216 @@
+import AutoreportBase from "./AutoreportBase";
+import Authentication from "./Authentication";
+import {Report, ReportError, ReportResponse, ReportSeverity, SystemInformation, SystemProcess} from "../types/Report";
+import {readFileSync, writeFileSync} from "node:fs";
+import UUID from "../types/UUID";
+import Notification from "./Notification";
+
+class APIEndpoint extends AutoreportBase {}
+
+export class ReportEndpoint extends APIEndpoint {
+ private static reports: Report[] = JSON.parse(readFileSync("./data/reports.json").toString());
+
+ public static refresh(req, res) {
+ ReportEndpoint.reports = JSON.parse(readFileSync("./data/reports.json").toString());
+ return res.status(200).json({
+ code: 200,
+ message: "OK."
+ });
+ }
+
+ public static post(req, res) {
+ if ([null, undefined, ""].includes(req.body.service) ||
+ [null, undefined, ""].includes(req.body.time) ||
+ [null, undefined, ""].includes(req.body.severity) ||
+ [null, undefined, "", {}].includes(req.body.error) ||
+ [null, undefined, "", {}].includes(req.body.systemInfo)) {
+ return res.status(400).json({
+ code: 400,
+ message: "Report information is missing."
+ });
+ }
+
+ if ([null, undefined, ""].includes(req.body.error.message) ||
+ [null, undefined, ""].includes(req.body.error.stacktrace)) {
+ return res.status(400).json({
+ code: 400,
+ message: "Error information is missing."
+ });
+ }
+
+ if ([null, undefined, ""].includes(req.body.systemInfo.pid) ||
+ [null, undefined, ""].includes(req.body.systemInfo.user) ||
+ [null, undefined, ""].includes(req.body.systemInfo.executable) ||
+ [null, undefined, ""].includes(req.body.systemInfo.memoryUsed) ||
+ [null, undefined, ""].includes(req.body.systemInfo.cpuTimes) ||
+ [null, undefined, ""].includes(req.body.systemInfo.uptime) ||
+ [null, undefined, ""].includes(req.body.systemInfo.systemUptime) ||
+ [null, undefined, ""].includes(req.body.systemInfo.os)) {
+ return res.status(400).json({
+ code: 400,
+ message: "System information is missing."
+ });
+ }
+
+ let severity = ReportSeverity.Medium;
+
+ switch (req.body.severity.toString().toLowerCase()) {
+ case "low":
+ case "0":
+ severity = ReportSeverity.Low;
+ break;
+ case "medium":
+ case "1":
+ severity = ReportSeverity.Medium;
+ break;
+ case "high":
+ case "2":
+ severity = ReportSeverity.High;
+ break;
+ case "critical":
+ case "3":
+ severity = ReportSeverity.Critical;
+ break;
+ case "fatal":
+ case "4":
+ severity = ReportSeverity.Fatal;
+ break;
+ default:
+ severity = ReportSeverity.Medium;
+ break;
+ }
+
+ let reportError: ReportError = {
+ message: req.body.error.message,
+ stacktrace: req.body.error.stacktrace,
+ logs: req.body.error.logs ?? null,
+ potentialFix: null
+ }
+
+ let systemInfo: SystemInformation = req.body.systemInfo;
+
+ let report: Report = {
+ id: new UUID(),
+ service: req.body.service,
+ time: new Date(req.body.time),
+ severity,
+ response: ReportResponse.None,
+ error: reportError,
+ systemInfo
+ }
+
+ ReportEndpoint.reports.push(report);
+ writeFileSync(AutoreportBase.getRoot() + "/data/reports.json", JSON.stringify(ReportEndpoint.reports));
+
+ let notification = new Notification(report);
+ notification.send().then(() => {
+ res.status(201).json({
+ code: 201,
+ message: "Created."
+ });
+ });
+ }
+
+ public static get(req, res) {
+ if (!Authentication.checkAuthentication(req)) return res.redirect("/oauth2/start");
+
+ if([null, undefined, ""].includes(req.query.id)) {
+ return res.status(400).json({
+ code: 400,
+ message: "An ID must be provided."
+ });
+ }
+
+ if(!ReportEndpoint.reports.some(report => report.id === req.query.id)) {
+ return res.status(404).json({
+ code: 404,
+ message: "Report with that ID does not exist."
+ });
+ }
+
+ res.status(200).json(ReportEndpoint.reports.find(report => report.id === req.query.id));
+ }
+
+ public static getMany(req, res) {
+ if (!Authentication.checkAuthentication(req)) return res.redirect("/oauth2/start");
+
+ let page = parseInt(req.query.page ?? 0);
+ let size = parseInt(req.query.size ?? 10);
+
+ if (isNaN(page)) {
+ return res.status(400).json({
+ code: 400,
+ message: "Page must be a number."
+ });
+ }
+ if (isNaN(size)) {
+ return res.status(400).json({
+ code: 400,
+ message: "Size must be a number."
+ });
+ }
+
+ // task: split the array into chunks of `size`
+ // good luck <3
+ let reportsRequested = ReportEndpoint.reports.slice((size * page), size);
+
+ res.status(200).json({
+ reports: reportsRequested,
+ page: page,
+ pageCount: Math.ceil(ReportEndpoint.reports.length / size)
+ });
+ }
+
+ public static patch(req, res) {
+ if (!Authentication.checkAuthentication(req)) return res.redirect("/oauth2/start");
+
+ if ([null, undefined, ""].includes(req.query.id) ||
+ [null, undefined, ""].includes(req.query.response)) {
+ return res.status(400).json({
+ code: 400,
+ message: "Report infomation is missing."
+ });
+ }
+
+ if(!ReportEndpoint.reports.some(report => report.id === req.query.id)) {
+ return res.status(404).json({
+ code: 404,
+ message: "That report does not exist."
+ });
+ }
+
+ let reportIndex = ReportEndpoint.reports.findIndex(report => report.id === req.query.id);
+
+ let response;
+
+ switch (req.query.response.toString().toLowerCase()) {
+ case "none":
+ case "0":
+ response = ReportResponse.None;
+ break;
+ case "acknowledged":
+ case "1":
+ response = ReportResponse.Acknowledged;
+ break;
+ case "ignored":
+ case "2":
+ response = ReportResponse.Ignored;
+ break;
+ case "stfu":
+ case "3":
+ response = ReportResponse.STFU;
+ break;
+ default:
+ response = ReportResponse.None;
+ break;
+ }
+
+ ReportEndpoint.reports[reportIndex].response = response;
+ writeFileSync(AutoreportBase.getRoot() + "/data/reports.json", JSON.stringify(ReportEndpoint.reports));
+
+ res.status(200).json({
+ code: 200,
+ message: "Updated."
+ });
+ }
+} \ No newline at end of file
diff --git a/src/core/Authentication.ts b/src/core/Authentication.ts
new file mode 100644
index 0000000..f298bdf
--- /dev/null
+++ b/src/core/Authentication.ts
@@ -0,0 +1,93 @@
+import AutoreportBase from "./AutoreportBase";
+import axios from "axios";
+import { readFileSync, writeFileSync } from "node:fs";
+import { randomBytes } from "crypto";
+
+export default class Authentication extends AutoreportBase {
+ public static getToken(token) {
+ let tokens = JSON.parse(readFileSync(AutoreportBase.getRoot() + "/data/tokens.json").toString());
+
+ if (Object.keys(tokens).includes(token) && new Date(tokens[token].date).getTime() - new Date().getTime() <= 31000000) {
+ return tokens[token].info;
+ } else {
+ return false;
+ }
+ }
+
+ private static saveToken(userInfo) {
+ let tokens = JSON.parse(readFileSync(AutoreportBase.getRoot() + "/data/tokens.json").toString());
+ let token = randomBytes(64).toString("base64url");
+
+ tokens[token] = {
+ date: new Date().getTime(),
+ info: userInfo
+ };
+ writeFileSync(AutoreportBase.getRoot() + "/data/tokens.json", JSON.stringify(tokens));
+
+ return token;
+ }
+
+ public static startFlow(req, res) {
+ res.redirect(`${AutoreportBase.config.authentication.server}/api/rest/oauth2/auth?client_id=${AutoreportBase.config.authentication.id}&response_type=code&redirect_uri=${AutoreportBase.config.authentication.redirect}&scope=Hub&request_credentials=default&access_type=offline`);
+ }
+
+ public static checkAuthentication(req) {
+ let _cookies = req.headers.cookie ?? "";
+ let _tokens = _cookies.split(";").map(i => i.trim().split("=")).filter(i => i[0] === "AutoreportToken");
+ let __tokens = _tokens[0] ?? [];
+ let token = __tokens[1] ?? null;
+
+ return !(!token || !this.getToken(token));
+ }
+
+ public static async callback(req, res) {
+ if (!req.query.code) {
+ res.redirect("/");
+ }
+
+ let token = (await axios.post(`${AutoreportBase.config.authentication.server}/api/rest/oauth2/token`, `grant_type=authorization_code&redirect_uri=${encodeURIComponent(AutoreportBase.config.authentication.redirect)}&code=${req.query.code}`, {
+ headers: {
+ 'Authorization': `Basic ${Buffer.from(`${AutoreportBase.config.authentication.id}:${AutoreportBase.config.authentication.secret}`).toString("base64")}`,
+ 'Accept': "application/json",
+ 'Content-Type': "application/x-www-form-urlencoded"
+ }
+ })).data.access_token;
+
+ let userInfo = (await axios.get(`${AutoreportBase.config.authentication.server}/api/rest/users/me`, {
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Accept': "application/json"
+ }
+ })).data;
+
+ let userToken = Authentication.saveToken(userInfo);
+ res.cookie('AutoreportToken', userToken, { maxAge: 31000000, httpOnly: true });
+ res.redirect("/");
+ }
+
+ public static testEndpoint(req, res) {
+ if (Authentication.checkAuthentication(req)) {
+ res.send("Authenticated");
+ } else {
+ res.send("NOT authenticated");
+ }
+ }
+
+ public static protectedAPI(req, res, next) {
+ if ([null, undefined, ""].includes(req.get("authorization"))) {
+ return res.status(401).json({
+ code: 401,
+ message: "Please provide an Authorization header."
+ });
+ }
+
+ if (req.get("authorization") !== AutoreportBase.config.api.token) {
+ return res.status(403).json( {
+ code: 403,
+ message: "You do not have permission to use this endpoint."
+ });
+ }
+
+ next();
+ }
+} \ No newline at end of file
diff --git a/src/core/Autoreport.ts b/src/core/Autoreport.ts
new file mode 100644
index 0000000..756c19f
--- /dev/null
+++ b/src/core/Autoreport.ts
@@ -0,0 +1,74 @@
+import AutoreportBase from "./AutoreportBase";
+import express from "express";
+import Authentication from "./Authentication";
+import {Report, ReportError, ReportResponse, ReportSeverity} from "../types/Report";
+import {readFileSync} from "node:fs";
+import UUID from "../types/UUID";
+import * as API from "./API";
+
+export default class Autoreport extends AutoreportBase {
+ constructor() {
+ const app = express();
+
+ app.use(express.static(AutoreportBase.getRoot() + "/assets"));
+ app.use(express.json());
+ app.set('view engine', 'ejs');
+
+ app.get("/", (req, res) => {
+ if (!Authentication.checkAuthentication(req)) return res.redirect("/oauth2/start");
+
+ let reports: Report[] = JSON.parse(readFileSync("./data/reports.json").toString())
+
+ res.render("index", { reports });
+ });
+
+ app.get("/oauth2/start", (req, res) => {
+ Authentication.startFlow(req, res);
+ });
+
+ app.get("/oauth2/callback", (req, res) => {
+ Authentication.callback(req, res);
+ });
+
+ app.post("/api/reports/refresh", (req, res) => {
+ API.ReportEndpoint.refresh(req, res);
+ })
+
+ // API methods (public)
+ app.get("/api/report", (req, res) => {
+ API.ReportEndpoint.get(req, res);
+ });
+
+ app.get("/api/reports", (req, res) => {
+ API.ReportEndpoint.getMany(req, res);
+ });
+
+ // API methods (private, need privateauth.equestria.dev authentication)
+ app.patch("/api/report", (req, res) => {
+ API.ReportEndpoint.patch(req, res);
+ });
+
+ // API methods (private, need token authentication)
+ app.post("/api/report", Authentication.protectedAPI, (req, res) => {
+ API.ReportEndpoint.post(req, res);
+ });
+
+ app.get("/oauth2/test", (req, res) => {
+ Authentication.testEndpoint(req, res);
+ })
+
+ app.listen(34512);
+
+ // To setup port forwarding:
+ // - Ctrl+Shift+K/Cmd+Shift+K
+ // - "Forward port"
+ // - "34512:34512"
+ // - You can now access it from http://localhost:34512
+
+ console.log("Listening!");
+ console.log(" - Public URL: http://localhost:34512");
+ console.log(" - OAuth2 test: http://localhost:34512/oauth2/start");
+
+ super();
+ }
+} \ No newline at end of file
diff --git a/src/core/AutoreportBase.ts b/src/core/AutoreportBase.ts
new file mode 100644
index 0000000..1129d35
--- /dev/null
+++ b/src/core/AutoreportBase.ts
@@ -0,0 +1,10 @@
+import YAML from "yaml";
+import { readFileSync } from "node:fs";
+
+export default class AutoreportBase {
+ public static getRoot() {
+ return __dirname + "/../../";
+ }
+
+ public static config = YAML.parse(readFileSync(AutoreportBase.getRoot() + "/config.yml").toString());
+} \ No newline at end of file
diff --git a/src/core/Notification.ts b/src/core/Notification.ts
new file mode 100644
index 0000000..0ab838d
--- /dev/null
+++ b/src/core/Notification.ts
@@ -0,0 +1,41 @@
+import AutoreportBase from "./AutoreportBase";
+import {Report, ReportSeverity} from "../types/Report";
+
+export default class Notification extends AutoreportBase {
+ service: string;
+ report: Report;
+
+ constructor(report: Report) {
+ super();
+ this.service = report.service;
+ this.report = report;
+ }
+
+ public async send() {
+ let message: string;
+
+ switch (this.report.severity) {
+ case ReportSeverity.Low: message = "Service " + this.service + " has encountered a minor error"; break;
+ case ReportSeverity.Medium: message = "Service " + this.service + " has encountered an error"; break;
+ case ReportSeverity.High: message = "Service " + this.service + " has encountered a major error"; break;
+ case ReportSeverity.Critical: message = "Service " + this.service + " has encountered a critical error"; break;
+ case ReportSeverity.Fatal: message = "Service " + this.service + " has encountered a fatal error"; break;
+ }
+
+ await fetch("https://" + AutoreportBase.config.notifications.server, {
+ method: "POST",
+ body: JSON.stringify({
+ topic: AutoreportBase.config.notifications.topic,
+ message,
+ title: "A service encountered an error",
+ tags: [ "crash", "service:" + this.service ],
+ priority: 3,
+ actions: [{ "action": "view", "label": "Open report", "url": AutoreportBase.config.base + "/#/report/" + this.report.id }]
+ }),
+ headers: {
+ "Authorization": "Basic " + Buffer.from(AutoreportBase.config.notifications.user + ":" + AutoreportBase.config.notifications.password).toString("base64"),
+ "Content-Type": "application/json"
+ }
+ })
+ }
+} \ No newline at end of file
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..b925d2f
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,32 @@
+import Autoreport from "./core/Autoreport";
+
+new Autoreport();
+
+// you need to add topic "crashes" to ntfy (make sure you select use another server)
+// mhm!
+// done
+// so when we'll test it it should work
+// mhm!
+
+
+// don't we need ejs?
+// probably, installing it
+// epic hehe
+// installed
+
+// is there a module you use to interface with the notifications?
+// it's just HTTP requests
+// okay, so here's the requirements
+// express, superagent, why not axios instead?
+// sure hehe
+// installed all that
+// epic, server time
+// oh wait we need dotenv to store the api token in .env
+// why not just use a config file
+// don't want to push the config file to the repo
+// add it to .gitignore
+// which i currently cannot see
+// because it doesn't exist -c-
+// oh
+// trying to not have a coughing fit while programming rn
+// .c. \ No newline at end of file
diff --git a/src/types/Report.ts b/src/types/Report.ts
new file mode 100644
index 0000000..c4ce342
--- /dev/null
+++ b/src/types/Report.ts
@@ -0,0 +1,68 @@
+import AutoreportBase from "../core/AutoreportBase";
+import UUID from "./UUID";
+
+export interface Report extends AutoreportBase {
+ id: UUID;
+ service: string;
+ time: Date;
+ severity: ReportSeverity;
+ response: ReportResponse;
+ error: ReportError;
+ systemInfo: SystemInformation;
+}
+
+export interface ReportError extends AutoreportBase {
+ message: string;
+ stacktrace: string;
+ logs?: string;
+ potentialFix: string|null;
+}
+
+export interface SystemInformation extends SystemProcess {
+ systemUptime: number;
+ os: string;
+}
+
+export interface SystemProcess extends AutoreportBase {
+ pid: number;
+ user: string;
+ executable: string;
+ memoryUsed: number;
+ cpuTime: number;
+ uptime: number;
+}
+
+export enum ReportSeverity {
+ // Low - something went wrong where it shouldn't, but it's not that bad
+ // Medium - something went wrong where it shouldn't, should be looked into
+ // High - something went wrong where it shouldn't, must be looked into
+ // Critical - something went wrong where it really shouldn't of, must be looked into right that moment
+ // Fatal - something went so wrong that the service will not recover without help, must be looked into right that moment
+
+ Low,
+ Medium,
+ High,
+ Critical,
+ Fatal
+}
+
+export enum ReportResponse {
+ // None - not been responded to yet
+ // Acknowledged - report has been acknowledged
+ // Ignored - report has been ignored
+ // STFU - "Shut The Fuck Up", we're already aware of this please stop telling us
+
+ None,
+ Acknowledged,
+ Ignored,
+ STFU
+}
+
+export enum ProcessState {
+ Stopped,
+ Running,
+ Starting,
+ Idle,
+ Blocked,
+ Stopping
+} \ No newline at end of file
diff --git a/src/types/UUID.ts b/src/types/UUID.ts
new file mode 100644
index 0000000..4a6c1eb
--- /dev/null
+++ b/src/types/UUID.ts
@@ -0,0 +1,7 @@
+import uuid from "uuid-v4";
+
+export default class UUID extends String {
+ constructor() {
+ super(uuid());
+ }
+} \ No newline at end of file