const express = require('express') const updateToken = require('../utils/token.js') const crypto = require('crypto') const db = require('../db') const authenticate = require('../middlewares').authenticate const logger = require('../logger')('routes-unit') const router = express.Router() // Base endpoint: /api/v1/unit router.get('/inbox', authenticate, (req, res) => { if (res.locals.authorised === false) { logger.warn('Unauthorised request to mailbox') res.status(401).json({ error: 'token expired or cannot be authenticated' }) return } const limit = 10 // this value equals the limit per page on a pwnas web interface db.inbox.totalMessages(res.locals.author.unit_ident[1], (err, count) => { if (err) { logger.error(err) res.status(500).json({ error: 'Internal Server Error' }) return } if (!count) { count = 0 } let page = req.query.p if (!page) { page = 1 } let offset = 0 if (page === 1) { offset = 0 } else { offset = (page * limit) - limit } let pages = Math.ceil(count / limit) const records = count db.inbox.messages(res.locals.author.unit_ident[1], limit, offset, (err, messages) => { if (err) { logger.error(err) res.status(500).json({ error: 'Internal Server Error' }) return } // Create the pages system pwngrid uses pages = Math.ceil(records / limit) const response = { pages, records, messages } res.status(200).json(response) }) }) }) router.get('/:fingerprint', authenticate, (req, res) => { // got unit search, from pwngrid binary // https://pwnagotchi.ai/api/grid/#get-api-v1-unit-fingerprint logger.info('Got unit search for ' + req.params.fingerprint) // Query fingerprint via mysql db.units.gridSearch(req.params.fingerprint, (err, unit) => { if (err) { logger.error(err) res.status(500).json({ error: 'Internal Server Error' }) return } if (!unit) { res.status(404).json({ error: 'Not Found' }) return } res.json(unit) }) }) router.get('/inbox/:messageId', authenticate, (req, res) => { if (res.locals.authorised) { db.inbox.message(req.params.messageId, res.locals.author.unit_ident[1], (err, message) => { if (err) { logger.error(err) res.status(500).json({ error: 'Internal Server Error' }) return } if (!message) { res.status(404).json({ error: 'Not Found' }) } else { res.json(message) } }) } else { res.status(401).json({ error: 'Unauthorised request' }) } }) router.get('/inbox/:messageId/:mark', authenticate, (req, res) => { if (req.params.mark === 'seen') { // mark message seen db.inbox.markMessageSeen(req.params.messageId, res.locals.author.unit_ident[1], (err) => { if (err) { logger.error(err) res.status(500).json({ error: 'Internal Server Error' }) return } logger.info('Updated Message') res.status(200).json({ status: 'success' }) }) } else if (req.params.mark === 'deleted') { db.inbox.deleteMessage(req.params.messageId, res.locals.author.unit_ident[1], (err) => { if (err) { logger.error(err) res.status(500).json({ error: 'Internal Server Error' }) return } logger.info('Updated Message') res.status(200).json({ status: 'success' }) }) } else if (req.params.mark === 'unseen') { db.inbox.markMessageUnseen(req.params.messageId, res.locals.author.unit_ident[1], (err) => { if (err) { logger.error(err) res.status(500).json({ error: 'Internal Server Error' }) return } logger.info('Updated Message') res.status(200).json({ status: 'success' }) }) } else { res.status(401).json({ error: 'Unauthorised request' }) logger.info('Unauthed Request to send a message') } }) router.post('/enroll', (req, res) => { // enroll sends // enrollment := map[string]interface{}{ // "identity": identity, // "public_key": pubKeyPEM64, // "signature": signature64, // "data": c.data, // } logger.info('Enroll from: ' + req.body.identity) if (!req.body.identity && !req.body.public_key && !req.body.signature) { res.status(422).json({ error: 'invalid body format' }) return } const identity = req.body.identity.split('@') // Before we enroll we want to check the device follows our methods of verifying identity let pubKey = Buffer.from(req.body.public_key, 'base64').toString() const KeyString = pubKey.split('RSA ').join('') const result = crypto.verify( 'rsa-sha256', new TextEncoder().encode(req.body.identity), { key: KeyString, padding: crypto.constants.RSA_PKCS1_PSS_PADDING }, Buffer.from(req.body.signature, 'base64')) if (!result) { logger.warn('Signature is NOT valid. A device has attempted to enroll that cannot verify its self.') res.status(401).json({ error: 'signature is invalid' }) return } else if (result) { logger.info('Signature is valid. continuing') } else { logger.error('Result is not true or false, how does this work?') res.status(401).json({ error: 'signature is invalid' }) return } // check if unit is already in our database db.units.search(identity[1], (err, units) => { if (err) { logger.error(err) res.status(500).json({ error: 'Internal Server Error' }) return } if (units.length === 0) { logger.info('Enrolling New') let country = 'XX' let addr = null let data = {} pubKey = Buffer.from(req.body.public_key, 'base64').toString() if (req && req.headers && req.headers['x-forwarded-for']) { addr = req.headers['x-forwarded-for'] } else { logger.warn('Error: X-Forwarded-For header is missing or undefined') addr = null } if (req && req.headers && req.headers['cf-ipcountry']) { country = req.headers['cf-ipcountry'] } else { logger.warn('Error: CF-IpCountry header is missing or undefined') country = 'XX' } if (req && req.body && req.body.data) { data = req.body.data } else { logger.warn('Error: data is missing or undefined') data = {} } db.units.add(identity[0], identity[1], pubKey, addr, country, JSON.stringify(data), (err, results) => { if (err) { logger.error(err) res.status(500).json({ error: 'Internal Server Error' }) return } logger.info('Enrolled new') res.status(200).send(JSON.stringify({ token: updateToken(identity, results.insertId) })) }) } else if (units.length === 1) { let data = {} if (req && req.body && req.body.data) { data = req.body.data } else { logger.error('Error: data is missing or undefined') data = {} } db.units.update(identity[1], identity[0], JSON.stringify(data), (err) => { if (err) { logger.error(err) res.status(500).json({ error: 'Internal Server Error' }) return } logger.info('Updating enrollee: ' + identity[1]) }) // TODO: should be removed res.status(200).send(JSON.stringify({ token: updateToken(identity, units.insertId) })) } else if (units.length > 1) { logger.info('Tried to enroll, but database return error or more than 1 match') res.status(500).json({ error: 'Internal Server Error. Please report this to @rai68 asap, as a id_rsa has been matched, which doesnt make sense.' }) } }) }) router.post('/report/ap', authenticate, (req, res) => { logger.info('AP incoming') if (res.locals.authorised === false) { logger.warn('Warning | Unauthorised device tried to send AP') res.status(401).json({ error: 'Unauthorised request' }) return } if (!req.body.bssid && !req.body.essid) { res.status(422).json({ error: 'Invalid body format' }) return } // Check if BSSID has been reported before db.accessPoints.search(req.body.bssid, (err, aps) => { if (err) { logger.error(err) res.status(500).json({ error: 'Internal Server Error' }) return } logger.info('Lets see if its been reported before') logger.info(aps.length) // If results is 0, the ap doesnt exist, but if its 1 or more, multiple devices have reported it. if (aps.length >= 1) { let reported = false for (const ap of aps) { if (ap.identity === res.locals.author.unit_ident[1]) { reported = true break } } if (reported === false) { logger.info('AP has not been reported before from this identity') // unit has not reported the AP before so continue to add it to db // add stuff here to include the AP even if its been reported, not sure how, maybe an array. Ok so now is adding another row for the same AP db.accessPoints.add(req.body.bssid, req.body.essid, res.locals.author.unit_ident[1], (err) => { if (err) { // Handle the error, but don't send a response here logger.error(err) res.status(500).json({ error: 'Internal Server Error' }) return } // Send a response when the insertion is successful res.status(200).json({ status: 'success' }) }) } else { logger.info('It has been reported sending 200, but not storing it again') res.status(200).json({ status: 'success' }) } } else if (aps.length === 0) { logger.info('it hasnt been reported before') // Because no APs exist with that SSID, add it to the database. db.accessPoints.add(req.body.bssid, req.body.essid, res.locals.author.unit_ident[1], (err) => { if (err) { // Handle the error, but don't send a response here logger.error(err) res.status(500).json({ error: 'Internal Server Error' }) return } // Send a response when the insertion is successful logger.info('sending 200 for a new AP') res.status(200).json({ status: 'success' }) }) } }) }) router.post('/report/aps', authenticate, (req, res) => { logger.info('AP received') if (res.locals.authorised === false) { logger.warn('Warning | Unauthorised device tried to send AP') res.status(401).json({ error: 'Unauthorised request' }) return } db.accessPoints.searchMultiple(req.body, (err, aps) => { if (err) { logger.error(err) res.status(500).json({ error: 'Internal Server Error' }) return } const bssids = aps.map(ap => Buffer.from(ap.bssid, 'binary').toString('utf-8')) const newAps = [] for (const ap of req.body) { const reportedIndex = bssids.indexOf(Buffer.from(ap.bssid.replace(/:/g, ''), 'hex').toString('utf-8')) if (reportedIndex > -1) { if (aps[reportedIndex].identity !== res.locals.author.unit_ident[1]) { logger.info('AP has not been reported before from this identity') newAps.push(ap) } } else { logger.info('AP first time reported') newAps.push(ap) } } if (newAps.length > 0) { db.accessPoints.addMultiple(newAps, res.locals.author.unit_ident[1], (err) => { if (err) { logger.error(err) res.status(500).json({ error: 'Internal Server Error' }) return } res.status(200).json({ status: 'success' }) }) } else { res.status(200).json({ status: 'success' }) } }) }) router.post('/:fingerprint/inbox', authenticate, (req, res) => { if (res.locals.authorised === false) { logger.warn('Warning | Unauthorised device tried to send MESSAGEP') res.status(401).json({ error: 'Unauthorised request' }) return } db.inbox.add(req.params.fingerprint, res.locals.author.unit_ident[0], res.locals.author.unit_ident[1], req.body.data, req.body.signature, (err) => { if (err) { logger.error(err) res.status(500).json({ error: 'Internal Server Error' }) return } res.status(200).json({ status: 'success' }) }) }) module.exports = router