@logux/server
Version:
Build own Logux server or make proxy between WebSocket and HTTP backend on any language
291 lines (260 loc) • 8.46 kB
JavaScript
import { LoguxError, parseId } from '@logux/core'
import cookie from 'cookie'
import fastq from 'fastq'
import semver from 'semver'
import { ALLOWED_META } from '../allowed-meta/index.js'
import { filterMeta } from '../filter-meta/index.js'
import { FilteredNode } from '../filtered-node/index.js'
async function onSend(action, meta) {
return [action, filterMeta(meta)]
}
function reportDetails(client) {
return {
connectionId: client.key,
nodeId: client.nodeId,
subprotocol: client.node.remoteSubprotocol
}
}
function denyBack(app, clientId, action, meta) {
app.emitter.emit('report', 'denied', { actionId: meta.id })
let [undoAction, undoMeta] = app.buildUndo(action, meta, 'denied')
undoMeta.clients = (undoMeta.clients || []).concat([clientId])
app.log.add(undoAction, undoMeta)
app.debugActionError(meta, `Action "${meta.id}" was denied`)
}
async function queueWorker(task, next) {
let { action, app, clientId, meta, onReceiveResolve, queue } = task
queue.next = next
let type = action.type
if (type === 'logux/subscribe' || type === 'logux/unsubscribe') {
return onReceiveResolve([action, meta])
}
let processor = app.getProcessor(type)
if (!processor) {
app.internalUnknownType(action, meta)
return onReceiveResolve(false)
}
let ctx = app.createContext(action, meta)
try {
let result = await processor.access(ctx, action, meta)
if (app.unknownTypes[meta.id]) {
delete app.unknownTypes[meta.id]
app.finally(processor, ctx, action, meta)
return false
} else if (!result) {
app.finally(processor, ctx, action, meta)
denyBack(app, clientId, action, meta)
return onReceiveResolve(false)
} else {
return onReceiveResolve([action, meta])
}
} catch (e) {
app.undo(action, meta, 'error')
app.emitter.emit('error', e, action, meta)
app.finally(processor, ctx, action, meta)
return onReceiveResolve(false)
}
}
export class ServerClient {
constructor(app, connection, key) {
this.app = app
this.userId = undefined
this.clientId = undefined
this.nodeId = undefined
this.processing = false
this.connection = connection
this.key = key.toString()
if (connection.ws) {
this.remoteAddress = connection.ws._socket.remoteAddress
this.httpHeaders = connection.ws.upgradeReq.headers
} else {
this.remoteAddress = '127.0.0.1'
this.httpHeaders = {}
}
this.node = new FilteredNode(this, app.nodeId, app.log, connection, {
auth: this.auth.bind(this),
onReceive: this.onReceive.bind(this),
onSend,
ping: app.options.ping,
subprotocol: app.options.subprotocol,
timeout: app.options.timeout
})
if (this.app.env === 'development') {
this.node.setLocalHeaders({ env: 'development' })
}
this.node.catch(err => {
err.connectionId = this.key
this.app.emitter.emit('error', err)
})
this.node.on('state', () => {
if (!this.node.connected && !this.destroyed) this.destroy()
})
this.node.on('clientError', err => {
if (err.type !== 'wrong-credentials') {
err.connectionId = this.key
this.app.emitter.emit('clientError', err)
}
})
this.app.emitter.emit('connected', this)
}
async auth(nodeId, token) {
this.nodeId = nodeId
let { clientId, userId } = parseId(nodeId)
this.clientId = clientId
this.userId = userId
if (this.app.options.supports) {
if (!this.isSubprotocol(this.app.options.supports)) {
throw new LoguxError('wrong-subprotocol', {
supported: this.app.options.supports,
used: this.node.remoteSubprotocol
})
}
}
if (nodeId === 'server' || userId === 'server') {
this.app.emitter.emit('unauthenticated', this, 0)
this.app.emitter.emit('report', 'unauthenticated', reportDetails(this))
return false
}
let ws = this.connection.ws
let headers = {}
if (ws && ws.upgradeReq && ws.upgradeReq.headers) {
headers = ws.upgradeReq.headers
}
let start = Date.now()
let result
try {
result = await this.app.authenticator({
client: this,
cookie: cookie.parse(headers.cookie || ''),
headers: this.node.remoteHeaders,
token,
userId: this.userId
})
} catch (e) {
if (e.name === 'LoguxError') {
throw e
} else {
e.nodeId = nodeId
this.app.emitter.emit('error', e)
result = false
}
}
if (this.app.isBruteforce(this.remoteAddress)) {
let e = new LoguxError('bruteforce')
e.nodeId = nodeId
this.app.emitter.emit('clientError', e)
result = false
}
if (result) {
let zombie = this.app.clientIds.get(this.clientId)
if (zombie) {
zombie.zombie = true
this.app.emitter.emit('report', 'zombie', { nodeId: zombie.nodeId })
zombie.destroy()
}
this.app.clientIds.set(this.clientId, this)
this.app.nodeIds.set(this.nodeId, this)
if (this.userId) {
if (!this.app.userIds.has(this.userId)) {
this.app.userIds.set(this.userId, [this])
} else {
this.app.userIds.get(this.userId).push(this)
}
}
this.app.emitter.emit('authenticated', this, Date.now() - start)
this.app.emitter.emit('report', 'authenticated', reportDetails(this))
} else {
this.app.emitter.emit('unauthenticated', this, Date.now() - start)
this.app.emitter.emit('report', 'unauthenticated', reportDetails(this))
this.app.rememberBadAuth(this.remoteAddress)
}
return result
}
destroy() {
this.destroyed = true
this.node.destroy()
if (this.userId) {
let users = this.app.userIds.get(this.userId)
if (users) {
users = users.filter(i => i !== this)
if (users.length === 0) {
this.app.userIds.delete(this.userId)
} else {
this.app.userIds.set(this.userId, users)
}
}
}
if (this.clientId) {
for (let channel in this.app.subscribers) {
let subscriber = this.app.subscribers[channel][this.nodeId]
if (subscriber) {
let action = { channel, type: 'logux/unsubscribe' }
let actionId = this.app.log.generateId()
let meta = { id: actionId, reasons: [], time: parseInt(actionId) }
this.app.performUnsubscribe(this.nodeId, action, meta)
}
}
this.app.clientIds.delete(this.clientId)
this.app.nodeIds.delete(this.nodeId)
}
if (!this.app.destroying) {
this.app.emitter.emit('disconnected', this)
}
this.app.connected.delete(this.key)
}
isSubprotocol(range) {
return semver.satisfies(this.node.remoteSubprotocol, range)
}
onReceive(action, meta) {
if (this.app.actionToQueue.has(meta.id)) {
return Promise.resolve(false)
}
let actionClientId = parseId(meta.id).clientId
let wrongUser = !this.clientId || this.clientId !== actionClientId
let wrongMeta = Object.keys(meta).some(i => !ALLOWED_META.includes(i))
if (wrongUser || wrongMeta) {
denyBack(this.app, this.clientId, action, meta)
return Promise.resolve(false)
}
return new Promise(resolve => {
let clientId = parseId(meta.id).clientId
let queueName = ''
let isChannel =
(action.type === 'logux/subscribe' ||
action.type === 'logux/unsubscribe') &&
action.channel
if (isChannel) {
for (let channel of this.app.channels) {
let pattern = channel.regexp || channel.pattern.regex
if (action.channel.match(pattern)) {
queueName = channel.queue
break
}
}
} else {
queueName = this.app.typeToQueue.get(action.type)
}
queueName = queueName || 'main'
let queueKey = `${clientId}/${queueName}`
let queue = this.app.queues.get(queueKey)
if (!queue) {
queue = fastq(queueWorker, 1)
this.app.queues.set(queueKey, queue)
}
if (!meta.subprotocol) {
meta.subprotocol = this.node.remoteSubprotocol
}
this.app.actionToQueue.set(meta.id, queueKey)
queue.push({
action,
app: this.app,
clientId,
meta,
onReceiveResolve: result => {
resolve(result)
},
queue
})
})
}
}