· 5 years ago · Apr 08, 2020, 07:26 PM
1const eWS = require("express-ws")
2const express = require("express")
3const WebSocket = require("ws")
4const sqlite = require("better-sqlite3")
5const ow = require("ow")
6const nanoID = require("nanoid")
7const R = require("ramda")
8
9const DB = sqlite("spudnet.sqlite3")
10
11DB.exec(`
12CREATE TABLE IF NOT EXISTS report_log (
13 id INTEGER PRIMARY KEY,
14 report TEXT NOT NULL,
15 sent_from TEXT NOT NULL, -- JSON
16 timestamp INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
17);
18
19CREATE TABLE IF NOT EXISTS keys (
20 id INTEGER PRIMARY KEY,
21 key TEXT NOT NULL UNIQUE,
22 issued INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
23 parent INTEGER REFERENCES keys(id),
24 enabled INTEGER NOT NULL DEFAULT 1,
25 bearer TEXT,
26 use TEXT,
27 permission_level INTEGER NOT NULL, -- permission level, children must have level <= parent, only used by applications
28 allowed_channels TEXT -- if NULL, then allowed on all channels; JSON array
29);
30
31CREATE TABLE IF NOT EXISTS command_log (
32 id INTEGER PRIMARY KEY,
33 channel TEXT NOT NULL,
34 command TEXT NOT NULL,
35 timestamp INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), -- unix epoch
36 key_used INTEGER NOT NULL REFERENCES keys(id)
37);
38`)
39
40const app = express()
41const expressWS = eWS(app)
42
43app.use(express.json())
44app.use(function(req, res, next) {
45 req.userAgent = req.get('User-Agent');
46 next();
47});
48
49const broadcast = (message, type, channel, sendingClient) => {
50 expressWS.getWss().clients.forEach(client => {
51 if (client.type === type && client.channel === channel && client.readyState == WebSocket.OPEN) {
52 if (client !== sendingClient) {
53 client.send(message)
54 }
55 }
56 })
57}
58
59app.ws("/:channel/", (ws, req) => {
60 const chan = req.params.channel
61 ws.channel = chan
62 ws.type = "client"
63 ws.on("message", msg => {
64 broadcast(msg, "admin", sys)
65 })
66})
67
68const insertCommandStatement = DB.prepare("INSERT INTO command_log (channel, command, key_used) VALUES (?, ?, ?)")
69const keyInfoQuery = DB.prepare("SELECT * FROM keys where key = ?")
70
71const checkAuth = (req, ws) => {
72 const key = /^[A-Za-z]* (.*)$/.exec(req.headers.authorization)[1]
73 const keyInfo = keyInfoQuery.get(key)
74 if (!keyInfo.enabled) {
75 ws.send("Key has been disabled")
76 ws.close()
77 }
78 if (!keyInfo) {
79 ws.send("Invalid key")
80 ws.close()
81 return false
82 }
83 const keyChannels = keyInfo.allowed_channels
84 if (!keyChannels) { return keyInfo }
85 else {
86 const channels = JSON.parse(keyChannels)
87 if (channels.includes(req.params.channel)) {
88 return keyInfo
89 } else {
90 ws.send("Key invalid for selected channel")
91 ws.close()
92 return false
93 }
94 }
95}
96
97app.ws("/:channel/admin", (ws, req) => {
98 const chan = req.params.channel
99 const auth = checkAuth(req, ws)
100 if (auth) {
101 ws.type = "admin"
102 ws.channel = chan
103 ws.on("message", msg => {
104 broadcast(msg, "client", chan)
105 insertCommandStatement.run(chan, msg, auth.id)
106 })
107 }
108})
109
110app.ws("/:channel/comm", (ws, req) => {
111 const chan = req.params.channel
112 const auth = checkAuth(req, ws)
113 if (auth) {
114 ws.type = "comm"
115 ws.channel = chan
116 ws.on("message", msg => {
117 broadcast(msg, "comm", chan, ws)
118 insertCommandStatement.run(chan + "/comm", msg, auth.id)
119 })
120 }
121})
122
123const insertReportStatement = DB.prepare("INSERT INTO report_log (report, sent_from) VALUES (?, ?)")
124
125app.post("/report/", (req, res) => {
126 const sentFrom = req.body.host;
127 const report = req.body.report;
128 if (typeof sentFrom !== "object" || !sentFrom || !report || typeof report !== "string") {
129 res.status(400).send("JSON required: host must be an object, report must be a string")
130 return
131 }
132 const result = insertReportStatement.run(report, JSON.stringify({
133 ...sentFrom,
134 ip: req.ip,
135 userAgent: req.userAgent
136 }))
137 res.send(result.lastInsertRowid.toString())
138})
139
140const getKeyInfo = key => {
141 const info = keyInfoQuery.get(key)
142 if (!info) { throw new Error("Key not found") }
143 if (info.enabled !== 1) { throw new Error("Key is disabled") }
144 info.enabled = info.enabled === 1 ? true : false
145 info.allowed_channels = JSON.parse(info.allowed_channels)
146 return info
147}
148
149app.post("/hki/key-info", (req, res) => {
150 const info = getKeyInfo(req.body.key)
151 res.json(info)
152})
153
154const insertKeyQuery = DB.prepare("INSERT INTO keys (key, parent, bearer, use, permission_level, allowed_channels) VALUES (?, ?, ?, ?, ?, ?)")
155// finds whether xs is a subset of ys
156const isSubset = (xs, ys) => R.all(elem => R.includes(elem, ys), xs)
157
158app.post("/hki/issue-key", (req, res) => {
159 const newInfo = req.body
160 const parent = getKeyInfo(req.body.key)
161 if (!parent.enabled) { throw new Error("Key is disabled") }
162 ow(newInfo, ow.object.exactShape({
163 allowed_channels: ow.optional.array.ofType(ow.string),
164 bearer: ow.optional.string,
165 permission_level: ow.optional.number.lessThanOrEqual(parent.permission_level),
166 use: ow.optional.string,
167 key: ow.string
168 }))
169 if (parent.allowed_channels && newInfo.allowed_channels && !isSubset(newInfo.allowed_channels, parent.allowed_channels)) {
170 throw new Error("allowed_channels must be subset of parent key allowed_channels")
171 }
172 const newKey = nanoID(128)
173 insertKeyQuery.run(newKey, parent.id, newInfo.bearer || parent.bearer, newInfo.use || parent.use,
174 newInfo.permission_level || parent.permission_level,
175 newInfo.allowed_channels === undefined ? null : JSON.stringify(newInfo.allowed_channels))
176 res.json(newKey)
177})
178
179const directChildrenQuery = DB.prepare("SELECT * FROM keys WHERE parent = ? AND enabled = 1")
180
181const dependentKeys = (id, by) => {
182 const children = directChildrenQuery.all(id)
183 return R.flatten(children.map(child => {
184 return [child[by], dependentKeys(child.id, by)]
185 }))
186}
187
188app.post("/hki/dependent-keys", (req, res) => {
189 const info = getKeyInfo(req.body.key)
190 res.json(dependentKeys(info.id, "key"))
191})
192
193const disableKeyQuery = DB.prepare("UPDATE keys SET enabled = 0 WHERE id = ?")
194const disableMany = DB.transaction(list => list.forEach(id =>
195 disableKeyQuery.run(id)
196))
197
198app.post("/hki/disable-key", (req, res) => {
199 const id = getKeyInfo(req.body.key).id
200 const toDisable = [id].concat(dependentKeys(id, "id"))
201 disableMany(toDisable)
202 res.json(toDisable)
203})
204
205app.use((err, req, res, next) => {
206 if (res.headersSent) {
207 return next(err)
208 }
209 res.status(500)
210 res.send(process.env.NODE_ENV === "production" ? err.toString() : err.stack)
211})
212app.set("trust proxy", "loopback")
213const port = parseInt(process.env.PORT) || 6086
214app.listen(port, () => console.log("listening on port", port))