154 lines
4.5 KiB
JavaScript
154 lines
4.5 KiB
JavaScript
|
#!/usr/bin/env node
|
||
|
import http from 'node:http';
|
||
|
import * as mqtt from "mqtt"
|
||
|
import { createHash, timingSafeEqual } from "node:crypto"
|
||
|
|
||
|
const settings = {
|
||
|
mqtt_host: process.env.MQTT_HOST || "localhost",
|
||
|
mqtt_username: process.env.MQTT_USER || "guest",
|
||
|
mqtt_password: process.env.MQTT_PASSWORD || "guest",
|
||
|
topic: process.env.TOPIC || "yorick/marvin/tracking",
|
||
|
api_hash: process.env.API_HASH || "z6mzC2TGdVCRuFE+oCrwj1GCHyP6OzYcPKZDiO/yLdqpmChC6S7ijCEUSY5gtqhpXhtYeDRyBjNeVJ/0Se4jQQ==",
|
||
|
listen_port: +process.env.PORT || 3000,
|
||
|
listen_host: process.env.HOST || "::",
|
||
|
}
|
||
|
|
||
|
const client = mqtt.connect(`mqtt://${settings.mqtt_host}`, {
|
||
|
username: settings.mqtt_username,
|
||
|
password: settings.mqtt_password
|
||
|
});
|
||
|
|
||
|
let currently_tracking = {}
|
||
|
|
||
|
client.subscribe(settings.topic)
|
||
|
|
||
|
client.on("connect", () => {
|
||
|
console.log("mqtt connected")
|
||
|
})
|
||
|
client.on("message", (topic, message) => {
|
||
|
if (topic == settings.topic) {
|
||
|
try {
|
||
|
currently_tracking = JSON.parse(message)
|
||
|
} catch(e) {
|
||
|
console.warn("unable to parse message", msg)
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
|
||
|
function checkKey(password) {
|
||
|
if (typeof password !== "string") return false
|
||
|
// would be nice if this took salt
|
||
|
const h = createHash("blake2b512")
|
||
|
// salt
|
||
|
h.update("O9yn_qX_jz68H-B6BrkEzRGAWfInzgeOmsCajTJVwcw=")
|
||
|
h.update(password)
|
||
|
// more salt
|
||
|
h.update("LHVV58vOGu7pKSV_Ofmes2joHCal6-F9UuhNLvOK7HM=")
|
||
|
const d = h.digest()
|
||
|
return timingSafeEqual(Buffer.from(settings.api_hash, "base64"), d)
|
||
|
}
|
||
|
|
||
|
async function waitForJSON(req, max_size) {
|
||
|
return new Promise((resolve, reject) => {
|
||
|
let body = [];
|
||
|
let accumulatedSize = 0;
|
||
|
|
||
|
req.on('data', (chunk) => {
|
||
|
accumulatedSize += chunk.length;
|
||
|
|
||
|
if (accumulatedSize > max_size) {
|
||
|
reject({statusCode: 413, message: 'Payload too large'});
|
||
|
req.connection.destroy();
|
||
|
} else {
|
||
|
body.push(chunk);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
req.on('end', () => {
|
||
|
try {
|
||
|
const parsedBody = JSON.parse(Buffer.concat(body).toString());
|
||
|
resolve(parsedBody);
|
||
|
} catch (e) {
|
||
|
reject({statusCode: 400, message: 'Bad Request'});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
req.on('error', (err) => {
|
||
|
reject({statusCode: 500, message: 'Internal Server Error'});
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
|
||
|
const supported_urls = ["startTracking", "stopTracking", "markDoneTask", "deleteTask", "editTask"]
|
||
|
|
||
|
async function setTrack(track) {
|
||
|
track.time = Date.now()
|
||
|
currently_tracking = track
|
||
|
await client.publishAsync(settings.topic, JSON.stringify(track), {
|
||
|
retain: true
|
||
|
})
|
||
|
}
|
||
|
|
||
|
const endpoints = {
|
||
|
async startTracking(task) {
|
||
|
await setTrack({ task, started: true })
|
||
|
},
|
||
|
async stopTracking(task) {
|
||
|
if (task.done) {
|
||
|
await setTrack({})
|
||
|
} else {
|
||
|
await setTrack({ task, started: false })
|
||
|
}
|
||
|
},
|
||
|
async markDoneTask(task) {
|
||
|
if (task._id == currently_tracking.task?._id)
|
||
|
await setTrack({})
|
||
|
},
|
||
|
async deleteTask(task) {
|
||
|
if (task._id == currently_tracking.task?._id)
|
||
|
await setTrack({})
|
||
|
},
|
||
|
async editTask(task) {
|
||
|
if (task._id == currently_tracking.task?._id)
|
||
|
await setTrack({ task, started: currently_tracking.started })
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// todo: check headers
|
||
|
async function handle(req, res) {
|
||
|
console.log(req.method, req.url)
|
||
|
if (endpoints[req.url.slice(1)]) {
|
||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||
|
res.setHeader('Access-Control-Allow-Methods', 'POST');
|
||
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Marv-Key');
|
||
|
if (req.method == "OPTIONS") {}
|
||
|
else if (req.method == "POST") {
|
||
|
if (!checkKey(req.headers["x-marv-key"])) {
|
||
|
throw { statusCode: 403, message: "auth required" }
|
||
|
}
|
||
|
const body = await waitForJSON(req, 1024 * 512) // 512 kb
|
||
|
await endpoints[req.url.slice(1)](body)
|
||
|
res.end()
|
||
|
} else {
|
||
|
throw { statusCode: 405, message: "Method not supported" }
|
||
|
}
|
||
|
res.end()
|
||
|
} else {
|
||
|
throw { statusCode: 404, message: "Unknown endpoint" }
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const server = http.createServer(async (req, res) => {
|
||
|
try {
|
||
|
await handle(req, res);
|
||
|
} catch (err) {
|
||
|
console.warn("error in request", err)
|
||
|
res.writeHead(err.statusCode || 500, {'Content-Type': 'text/plain'}).end(err.message || 'Internal Server Error');
|
||
|
}
|
||
|
})
|
||
|
|
||
|
|
||
|
server.listen(settings.listen_port, settings.listen_host, () => {
|
||
|
console.log(`Server running on http://${settings.listen_host}:${settings.listen_port}/`);
|
||
|
});
|