Skip to content

Commit

Permalink
Merge pull request #166 from harmony-one/basic-rate-limit
Browse files Browse the repository at this point in the history
Basic key protection
  • Loading branch information
theofandrich authored Nov 13, 2023
2 parents 4fa80b5 + e613c56 commit 317cf9f
Show file tree
Hide file tree
Showing 20 changed files with 335 additions and 19 deletions.
6 changes: 6 additions & 0 deletions voice/relay/deploy/enable.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/sh
sudo setcap CAP_NET_BIND_SERVICE=+eip /usr/bin/node
sudo cp voice-ai-relay.service /etc/systemd/system/voice-ai-relay.service
sudo systemctl start voice-ai-relay
sudo systemctl enable voice-ai-relay
systemctl status voice-ai-relay
2 changes: 2 additions & 0 deletions voice/relay/deploy/log.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/sh
journalctl -u voice-ai-relay -n 1000 -f
8 changes: 8 additions & 0 deletions voice/relay/deploy/port.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
!#/bin/sh
sudo iptables -A INPUT -i eth0 -p tcp --dport 80 -j ACCEPT
sudo iptables -A INPUT -i eth0 -p tcp --dport 3000 -j ACCEPT
sudo iptables -A INPUT -i eth0 -p tcp --dport 443 -j ACCEPT
sudo iptables -A INPUT -i eth0 -p tcp --dport 8443 -j ACCEPT
sudo iptables -A PREROUTING -t nat -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 3000
sudo iptables -A PREROUTING -t nat -i eth0 -p tcp --dport 443 -j REDIRECT --to-port 8443
sudo iptables --flush
15 changes: 15 additions & 0 deletions voice/relay/deploy/voice-ai-relay.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[Unit]
Description=Voice AI Relay Server
Documentation=https://github.com/harmony-one/x/
After=network.target

[Service]
Environment=PORT=80 HTTPS_PORT=443
Type=simple
User=worker
WorkingDirectory=/opt/git/x/voice/relay
ExecStart=/bin/bash -c "source ~/.profile;/usr/bin/node --loader ts-node/esm ./bin/run.ts"
Restart=on-failure

[Install]
WantedBy=multi-user.target
8 changes: 7 additions & 1 deletion voice/relay/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,26 @@
"dependencies": {
"@simplewebauthn/server": "^8.3.2",
"@types/cookie-parser": "^1.4.4",
"@types/request-ip": "^0.0.41",
"axios": "^1.5.1",
"cbor": "^9.0.1",
"compression": "^1.7.4",
"cookie-parser": "^1.4.6",
"cookie-session": "^2.0.0",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-rate-limit": "^7.1.4",
"fast-sha256": "^1.3.0",
"http-errors": "^2.0.0",
"jsrsasign": "^10.8.6",
"morgan": "^1.10.0",
"node-cache": "^5.1.2",
"openai": "^4.14.0"
"openai": "^4.14.0",
"request-ip": "^3.3.0"
},
"devDependencies": {
"@types/compression": "^1.7.5",
"@types/cookie": "^0.5.2",
"@types/cookie-session": "^2.0.45",
"@types/cors": "^2.8.14",
Expand Down
28 changes: 25 additions & 3 deletions voice/relay/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import express from 'express'
import express, { type Response, type Request, type NextFunction } from 'express'
import cookieParser from 'cookie-parser'
import morgan from 'morgan'
import apiRouter from './routes/index.js'
import createError from 'http-errors'
// import apiRouter from './routes/hard.js'
import softRateLimitedApiRouter from './routes/soft.js'
import fs from 'fs'
import http, { type Server as HttpServer } from 'http'
import https from 'https'
import config from './config/index.js'
import cors from 'cors'
import compression from 'compression'
import requestIp from 'request-ip'

const app = express()
let httpServer: HttpServer
Expand All @@ -30,6 +34,8 @@ if (config.https.only) {
httpServer = http.createServer(app)
}
const httpsServer = https.createServer(httpsOptions, app)
app.use(compression())
app.use(requestIp.mw())

app.use(morgan('common'))
app.use(cookieParser())
Expand Down Expand Up @@ -58,7 +64,23 @@ app.options('*', async (_req, res) => {
res.end()
})

app.use('/', apiRouter)
app.use('/soft', softRateLimitedApiRouter)

// catch 404 and forward to error handler
app.use(function (req, res, next) {
next(createError(404))
})

// error handler
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
// set locals, only providing error in development
res.locals.message = err.message
res.locals.error = config.debug ? err : ''

// render the error page
res.status(500)
res.json({ error: res.locals.error, message: err.message, stack: config.debug ? err.stack : undefined })
})

export {
httpServer,
Expand Down
9 changes: 8 additions & 1 deletion voice/relay/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@ const config = {
packageName: process.env.PACKAGE_NAME ?? '',
openai: { key: process.env.OPENAI_KEY ?? '' },
deepgram: { key: process.env.DEEPGRAM_KEY ?? '' },
playht: { key: process.env.PLAYHT_KEY ?? '' },
playht: { key: process.env.PLAYHT_KEY ?? '' }
}

export const OpenAIDistributedKeys: string[] = JSON.parse(process.env.OPENAI_DISTRIBUTED_KEYS ?? '[]')
export const BlockedDeviceIds: string[] = JSON.parse(process.env.BLOCKED_DEVICE_IDS ?? '[]')
export const BlockedIps: string[] = JSON.parse(process.env.BLOCKED_IPS ?? '[]')

export const SharedEncryptionSecret: string = process.env.SHARED_ENCRYPTION_SECRET ?? ''
export const SharedEncryptionIV: string = process.env.SHARED_ENCRYPTION_IV ?? ''

export default config
File renamed without changes.
66 changes: 66 additions & 0 deletions voice/relay/src/routes/soft.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Router, type Request, type Response, type NextFunction } from 'express'
import { HttpStatusCode } from 'axios'
import rateLimit, { type Options as RLOptions, type RateLimitRequestHandler } from 'express-rate-limit'
import { BlockedDeviceIds, BlockedIps, OpenAIDistributedKeys } from '../config/index.js'
import { encrypt, hexView, stringToBytes } from '../utils.js'
import { hash as sha256 } from 'fast-sha256'
const router: Router = Router()

const deviceLimiter = (args?: RLOptions): RateLimitRequestHandler => rateLimit({
windowMs: 1000 * 60,
limit: 10,
keyGenerator: req => req.header('x-device-token') ?? '',
...args
})

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const ipLimiter = (args?: RLOptions): RateLimitRequestHandler => rateLimit({
windowMs: 1000 * 60,
limit: 10,
keyGenerator: req => req.clientIp ?? 'N/A',
...args
})

const parseDeviceToken = (req: Request, res: Response, next: NextFunction): any => {
const deviceToken = req.header('x-device-token')
if (!deviceToken) {
res.status(HttpStatusCode.Forbidden).json({ error: 'device unsupported', code: 100 })
return
}
const deviceTokenHash = hexView(sha256(stringToBytes(deviceToken)))
if (BlockedDeviceIds.includes(deviceTokenHash)) {
res.status(HttpStatusCode.Forbidden).json({ error: 'device banned', code: 101 })
return
}
req.deviceToken = deviceToken
req.deviceTokenHash = deviceTokenHash
next()
}

const checkIpBan = (req: Request, res: Response, next: NextFunction): any => {
if (!req.clientIp) {
console.error('[checkIpBan] Cannot find ip of request', req)
}
if (BlockedIps.includes(req.clientIp ?? 'N/A')) {
res.status(HttpStatusCode.Forbidden).json({ error: 'ip banned', code: 102 })
}
next()
}

router.get('/health', (req, res) => {
res.send('OK')
})

router.get('/key', parseDeviceToken, checkIpBan, deviceLimiter(), ipLimiter(), (req, res) => {
// TODO: validate the device token, https://developer.apple.com/documentation/devicecheck/accessing_and_modifying_per-device_data
const numKeys = BigInt(OpenAIDistributedKeys.length)
const keyIndex = Number(BigInt('0x' + req.deviceTokenHash) % numKeys)
const key = OpenAIDistributedKeys[keyIndex]
const encryptedKey = encrypt(key)
const encoded = encryptedKey.toString('base64')

console.log(`[deviceTokenHash=${req.deviceTokenHash}][ip=${req.clientIp}] Provided encryptedKey ${encoded}`)
res.json({ key: encryptedKey.toString('base64') })
})

export default router
15 changes: 15 additions & 0 deletions voice/relay/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import crypto from 'crypto'
import { SharedEncryptionIV, SharedEncryptionSecret } from './config/index.js'
import { hash as sha256 } from 'fast-sha256'

export const hexView = (bytes: Buffer | Uint8Array): string => {
return bytes && Array.from(bytes).map(x => x.toString(16).padStart(2, '0')).join('')
}
Expand All @@ -20,3 +24,14 @@ export function chunkstr (str: string, size: number): string[] {

return chunks
}

const aesKey = sha256(stringToBytes(SharedEncryptionSecret)).slice(0, 32)
const aesIv = sha256(stringToBytes(SharedEncryptionIV)).slice(0, 16)

// console.log('aesKey', aesKey)
// console.log('aesIv', aesIv)

export function encrypt (s: string): Buffer {
const cipher = crypto.createCipheriv('aes-256-cbc', aesKey, aesIv)
return Buffer.concat([cipher.update(s, 'utf8'), cipher.final()])
}
3 changes: 2 additions & 1 deletion voice/relay/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
"strict": true, /* Enable all strict type-checking options. */
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
"declaration": true
"declaration": true,
"typeRoots": ["./types"]
}
}
10 changes: 10 additions & 0 deletions voice/relay/types/express/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
declare global {
namespace Express {
interface Request {
deviceToken: string
deviceTokenHash: string
}
}
}

export {}
55 changes: 52 additions & 3 deletions voice/relay/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,13 @@
"@types/connect" "*"
"@types/node" "*"

"@types/compression@^1.7.5":
version "1.7.5"
resolved "https://registry.yarnpkg.com/@types/compression/-/compression-1.7.5.tgz#0f80efef6eb031be57b12221c4ba6bc3577808f7"
integrity sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg==
dependencies:
"@types/express" "*"

"@types/connect@*":
version "3.4.37"
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.37.tgz#c66a96689fd3127c8772eb3e9e5c6028ec1a9af5"
Expand Down Expand Up @@ -371,6 +378,13 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.6.tgz#7cb33992049fd7340d5b10c0098e104184dfcd2a"
integrity sha512-+0autS93xyXizIYiyL02FCY8N+KkKPhILhcUSA276HxzreZ16kl+cmwvV2qAM/PuCCwPXzOXOWhiPcw20uSFcA==

"@types/request-ip@^0.0.41":
version "0.0.41"
resolved "https://registry.yarnpkg.com/@types/request-ip/-/request-ip-0.0.41.tgz#c22a3244df2573402989346062851b06b7a5ac4e"
integrity sha512-Qzz0PM2nSZej4lsLzzNfADIORZhhxO7PED0fXpg4FjXiHuJ/lMyUg+YFF5q8x9HPZH3Gl6N+NOM8QZjItNgGKg==
dependencies:
"@types/node" "*"

"@types/semver@^7.3.12":
version "7.5.4"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.4.tgz#0a41252ad431c473158b22f9bfb9a63df7541cff"
Expand Down Expand Up @@ -484,7 +498,7 @@ abort-controller@^3.0.0:
dependencies:
event-target-shim "^5.0.0"

accepts@~1.3.8:
accepts@~1.3.5, accepts@~1.3.8:
version "1.3.8"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==
Expand Down Expand Up @@ -709,6 +723,11 @@ builtins@^5.0.1:
dependencies:
semver "^7.0.0"

[email protected]:
version "3.0.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==

[email protected]:
version "3.1.2"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
Expand Down Expand Up @@ -802,6 +821,26 @@ combined-stream@^1.0.8:
dependencies:
delayed-stream "~1.0.0"

compressible@~2.0.16:
version "2.0.18"
resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==
dependencies:
mime-db ">= 1.43.0 < 2"

compression@^1.7.4:
version "1.7.4"
resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f"
integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==
dependencies:
accepts "~1.3.5"
bytes "3.0.0"
compressible "~2.0.16"
debug "2.6.9"
on-headers "~1.0.2"
safe-buffer "5.1.2"
vary "~1.1.2"

[email protected]:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
Expand Down Expand Up @@ -1329,6 +1368,11 @@ event-target-shim@^5.0.0:
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==

express-rate-limit@^7.1.4:
version "7.1.4"
resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-7.1.4.tgz#c321fe186a8366eacdb2c5edf2ad6a2f6d93e576"
integrity sha512-mv/6z+EwnWpr+MjGVavMGvM4Tl8S/tHmpl9ZsDfrQeHpYy4Hfr0UYdKEf9OOTe280oIr70yPxLRmQ6MfINfJDw==

express@^4.18.2:
version "4.18.2"
resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59"
Expand Down Expand Up @@ -1650,7 +1694,7 @@ hasown@^2.0.0:
dependencies:
function-bind "^1.1.2"

[email protected]:
[email protected], http-errors@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==
Expand Down Expand Up @@ -1987,7 +2031,7 @@ micromatch@^4.0.4:
braces "^3.0.2"
picomatch "^2.3.1"

[email protected]:
[email protected], "mime-db@>= 1.43.0 < 2":
version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
Expand Down Expand Up @@ -2334,6 +2378,11 @@ regexpp@^3.0.0:
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==

request-ip@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/request-ip/-/request-ip-3.3.0.tgz#863451e8fec03847d44f223e30a5d63e369fa611"
integrity sha512-cA6Xh6e0fDBBBwH77SLJaJPBmD3nWVAcF9/XAcsrIHdjhFzFiB5aNQFytdjCGPezU3ROwrR11IddKAM08vohxA==

require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
Expand Down
Loading

0 comments on commit 317cf9f

Please sign in to comment.