const path = require('path') const express = require('express') const bodyParser = require('body-parser') require('express-async-errors') const config = require(path.join(__dirname, 'config.js')) const app = express() const port = config.api.port const host = config.api.host const mount = config.api.mount app.enable('trust proxy', 1) app.disable('x-powered-by') app.use(bodyParser.urlencoded({ extended: false })) app.use(bodyParser.json()) const apiRouter = express.Router() const internalRouter = express.Router() // Make sure internal router can only be accessed from local network internalRouter.use(function (req, res, next) { if (req.ip !== '127.0.0.1' && req.ip !== '::1') { return res.status(401).end('401 Unauthorized. Accessed internal API endpoint!') } next() }) // Call this here to prevent key from being required on internal endpoints apiRouter.use('/internal', internalRouter) // Key-based authorization for API endpoints apiRouter.use(function (req, res, next) { let key = req.query.key // Prefer headers for authentication if (req.headers['authorization'] && typeof req.headers['authorization'] === 'string') { let auth = req.headers['authorization'] if (auth.indexOf('Bearer') !== -1) return next(new Error('Invalid authorization header!')) auth = auth.split(' ')[1] if (!auth) return next(new Error('Invalid authorization header!')) key = auth } if (!key || config.api.keys.indexOf(key) === -1) return res.status(403).end('403 Forbidden') req.key = key next() }) module.exports = function (liq, sched) { // Schedule actions apiRouter.get('/events', (req, res) => { res.jsonp({ calendar: config.calendar.calendar, events: sched.events }) }) apiRouter.post('/events/reload', async (req, res) => { await sched.calendarFetch() await sched.update() res.status(204).end() }) // Liquidsoap status apiRouter.get('/', (req, res) => { res.jsonp({ time: new Date(), authorization: req.key, liquidsoap: { status: liq.running, meta: liq.metadata } }) }) apiRouter.get('/meta', (req, res) => { res.jsonp(liq.metadata) }) // Liquidsoap actions apiRouter.post('/queue', (req, res) => { let items = req.body.items if (typeof items === 'string') items = [items] else if (!items || !items.length) return res.status(400).end('400 Bad Request') liq.queue(items) res.status(202).end('OK') }) apiRouter.post('/skip', (req, res) => { liq.skip() res.status(202).end('OK') }) // Internal router (Only accessible from localhost, no matter the host setting) // Used by liquidsoap internalRouter.post('/meta', (req, res) => { let m = req.body if (Object.keys(m).length === 0) { return res.send('(none)') } liq.metadata = m res.send(JSON.stringify(m, null, 1)) }) app.use(mount, apiRouter) // Handle 404 app.use((req, res, next) => { res.status(404).end('404 Not Found') }) // Handle errors app.use((error, req, res, next) => { console.error(error.stack) if (app.get('env') === 'production') { return res.status(500).end('Internal Server Error: ' + error.message) } res.status(500).end(error.stack) }) // And finally, listen on the configured port and host app.listen(port, host, () => console.log(`API server listening on ${host}:${port}${mount}`)) }