2021-08-03 22:02:59 +03:00
|
|
|
//
|
|
|
|
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
|
|
|
// Copyright © 2021 Hardcore Engineering Inc.
|
|
|
|
//
|
|
|
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License. You may
|
|
|
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
//
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
//
|
|
|
|
|
2021-08-04 19:17:01 +03:00
|
|
|
import { readRequest, serialize, Response } from '@anticrm/platform'
|
2021-08-28 10:08:41 +03:00
|
|
|
import type { Token } from '@anticrm/server-core'
|
2021-08-03 22:02:59 +03:00
|
|
|
import { createServer, IncomingMessage } from 'http'
|
|
|
|
import WebSocket, { Server } from 'ws'
|
|
|
|
import { decode } from 'jwt-simple'
|
|
|
|
|
2021-10-13 19:46:48 +03:00
|
|
|
import type { Doc, Ref, Class, FindOptions, FindResult, Tx, DocumentQuery, Storage, ServerStorage, TxResult } from '@anticrm/core'
|
2021-08-11 12:35:56 +03:00
|
|
|
|
|
|
|
let LOGGING_ENABLED = true
|
|
|
|
|
|
|
|
export function disableLogging (): void { LOGGING_ENABLED = false }
|
|
|
|
|
|
|
|
class Session implements Storage {
|
|
|
|
constructor (
|
|
|
|
private readonly manager: SessionManager,
|
2021-08-28 10:08:41 +03:00
|
|
|
private readonly token: Token,
|
2021-08-11 12:35:56 +03:00
|
|
|
private readonly storage: ServerStorage
|
|
|
|
) {}
|
|
|
|
|
2021-08-14 12:11:49 +03:00
|
|
|
async ping (): Promise<string> { console.log('ping'); return 'pong!' }
|
2021-08-11 13:07:49 +03:00
|
|
|
|
2021-08-11 12:35:56 +03:00
|
|
|
async findAll <T extends Doc>(_class: Ref<Class<T>>, query: DocumentQuery<T>, options?: FindOptions<T>): Promise<FindResult<T>> {
|
|
|
|
return await this.storage.findAll(_class, query, options)
|
|
|
|
}
|
|
|
|
|
2021-10-13 19:46:48 +03:00
|
|
|
async tx (tx: Tx): Promise<TxResult> {
|
2021-10-13 21:58:14 +03:00
|
|
|
const [result, derived] = await this.storage.tx(tx)
|
2021-08-11 12:35:56 +03:00
|
|
|
this.manager.broadcast(this, this.token, { result: tx })
|
|
|
|
for (const tx of derived) {
|
2021-10-10 14:51:59 +03:00
|
|
|
this.manager.broadcast(null, this.token, { result: tx })
|
2021-08-11 12:35:56 +03:00
|
|
|
}
|
2021-10-13 21:58:14 +03:00
|
|
|
return result
|
2021-08-11 12:35:56 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
interface Workspace {
|
|
|
|
storage: ServerStorage
|
|
|
|
sessions: [Session, WebSocket][]
|
|
|
|
}
|
|
|
|
|
|
|
|
class SessionManager {
|
|
|
|
private readonly workspaces = new Map<string, Workspace>()
|
|
|
|
|
2021-08-28 10:08:41 +03:00
|
|
|
async addSession (ws: WebSocket, token: Token, storageFactory: (ws: string) => Promise<ServerStorage>): Promise<Session> {
|
2021-08-11 12:35:56 +03:00
|
|
|
const workspace = this.workspaces.get(token.workspace)
|
|
|
|
if (workspace === undefined) {
|
2021-08-19 13:35:56 +03:00
|
|
|
const storage = await storageFactory(token.workspace)
|
2021-08-11 12:35:56 +03:00
|
|
|
const session = new Session(this, token, storage)
|
|
|
|
const workspace: Workspace = {
|
|
|
|
storage,
|
|
|
|
sessions: [[session, ws]]
|
|
|
|
}
|
|
|
|
this.workspaces.set(token.workspace, workspace)
|
|
|
|
return session
|
|
|
|
} else {
|
|
|
|
const session = new Session(this, token, workspace.storage)
|
|
|
|
workspace.sessions.push([session, ws])
|
|
|
|
return session
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-28 10:08:41 +03:00
|
|
|
close (ws: WebSocket, token: Token, code: number, reason: string): void {
|
2021-08-11 12:35:56 +03:00
|
|
|
if (LOGGING_ENABLED) console.log(`closing websocket, code: ${code}, reason: ${reason}`)
|
|
|
|
const workspace = this.workspaces.get(token.workspace)
|
|
|
|
if (workspace === undefined) {
|
|
|
|
throw new Error('internal: cannot find sessions')
|
|
|
|
}
|
|
|
|
workspace.sessions = workspace.sessions.filter(session => session[1] !== ws)
|
|
|
|
if (workspace.sessions.length === 0) {
|
|
|
|
if (LOGGING_ENABLED) console.log('no sessions for workspace', token.workspace)
|
|
|
|
this.workspaces.delete(token.workspace)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-10 14:51:59 +03:00
|
|
|
broadcast (from: Session | null, token: Token, resp: Response<any>): void {
|
2021-08-11 12:35:56 +03:00
|
|
|
const workspace = this.workspaces.get(token.workspace)
|
|
|
|
if (workspace === undefined) {
|
|
|
|
throw new Error('internal: cannot find sessions')
|
|
|
|
}
|
|
|
|
if (LOGGING_ENABLED) console.log(`server broadcasting to ${workspace.sessions.length} clients...`)
|
|
|
|
const msg = serialize(resp)
|
|
|
|
for (const session of workspace.sessions) {
|
|
|
|
if (session[0] !== from) { session[1].send(msg) }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-03 22:02:59 +03:00
|
|
|
async function handleRequest<S> (service: S, ws: WebSocket, msg: string): Promise<void> {
|
|
|
|
const request = readRequest(msg)
|
|
|
|
const f = (service as any)[request.method]
|
2021-08-09 00:04:39 +03:00
|
|
|
const result = await f.apply(service, request.params)
|
|
|
|
const resp = { id: request.id, result }
|
|
|
|
ws.send(serialize(resp))
|
2021-08-03 22:02:59 +03:00
|
|
|
}
|
|
|
|
|
2021-08-04 19:17:01 +03:00
|
|
|
/**
|
|
|
|
* @public
|
|
|
|
* @param sessionFactory -
|
2021-08-04 12:10:22 +03:00
|
|
|
* @param port -
|
|
|
|
* @param host -
|
2021-08-03 22:02:59 +03:00
|
|
|
*/
|
2021-11-22 14:17:10 +03:00
|
|
|
export function start (storageFactory: (workspace: string) => Promise<ServerStorage>, port: number, host?: string): () => void {
|
2021-08-04 23:47:15 +03:00
|
|
|
console.log(`starting server on port ${port} ...`)
|
2021-08-03 22:02:59 +03:00
|
|
|
|
2021-08-11 12:35:56 +03:00
|
|
|
const sessions = new SessionManager()
|
2021-08-03 22:02:59 +03:00
|
|
|
|
|
|
|
const wss = new Server({ noServer: true })
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
2021-08-28 10:08:41 +03:00
|
|
|
wss.on('connection', async (ws: WebSocket, request: any, token: Token) => {
|
2021-08-19 20:34:58 +03:00
|
|
|
const buffer: string[] = []
|
|
|
|
|
|
|
|
ws.on('message', (msg: string) => { buffer.push(msg) })
|
2021-08-11 12:35:56 +03:00
|
|
|
const session = await sessions.addSession(ws, token, storageFactory)
|
2021-08-03 22:02:59 +03:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
2021-08-11 12:35:56 +03:00
|
|
|
ws.on('message', async (msg: string) => await handleRequest(session, ws, msg))
|
|
|
|
ws.on('close', (code: number, reason: string) => sessions.close(ws, token, code, reason))
|
2021-08-19 20:34:58 +03:00
|
|
|
|
|
|
|
for (const msg of buffer) {
|
|
|
|
await handleRequest(session, ws, msg)
|
|
|
|
}
|
2021-08-03 22:02:59 +03:00
|
|
|
})
|
|
|
|
|
|
|
|
const server = createServer()
|
2021-08-07 08:58:28 +03:00
|
|
|
server.on('upgrade', (request: IncomingMessage, socket: any, head: Buffer) => {
|
2021-08-03 22:02:59 +03:00
|
|
|
const token = request.url?.substring(1) // remove leading '/'
|
2021-08-04 10:56:34 +03:00
|
|
|
try {
|
|
|
|
const payload = decode(token ?? '', 'secret', false)
|
2021-08-09 00:04:39 +03:00
|
|
|
console.log('client connected with payload', payload)
|
2021-08-04 10:56:34 +03:00
|
|
|
wss.handleUpgrade(request, socket, head, ws => wss.emit('connection', ws, request, payload))
|
|
|
|
} catch (err) {
|
2021-08-11 12:35:56 +03:00
|
|
|
console.log('unauthorized client')
|
2021-08-03 22:02:59 +03:00
|
|
|
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n')
|
|
|
|
socket.destroy()
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
server.listen(port, host)
|
2021-11-22 14:17:10 +03:00
|
|
|
return () => {
|
|
|
|
server.close()
|
|
|
|
}
|
2021-08-03 22:02:59 +03:00
|
|
|
}
|