1
0
Fork 0
mirror of synced 2024-09-30 17:18:14 +13:00

Rewrite how middlewares are ran inside sockets, and fix collaboration

This commit is contained in:
Andrew Kingston 2023-06-27 11:33:23 +01:00
parent 9e3711a4c6
commit cfdd6bafb7
3 changed files with 133 additions and 68 deletions

View file

@ -1,12 +1,15 @@
import authorized from "../middleware/authorized"
import currentApp from "../middleware/currentapp"
import { BaseSocket } from "./websocket"
import { context, permissions } from "@budibase/backend-core"
import { auth, permissions } from "@budibase/backend-core"
import http from "http"
import Koa from "koa"
import { getTableId } from "../api/controllers/row/utils"
import { Row, Table } from "@budibase/types"
import { Socket } from "socket.io"
import { GridSocketEvent } from "@budibase/shared-core"
import { userAgent } from "koa-useragent"
import { createContext, runMiddlewares } from "./middleware"
const { PermissionType, PermissionLevel } = permissions
@ -26,28 +29,27 @@ export default class GridSocket extends BaseSocket {
return
}
// Check if the user has permission to read this resource
const middleware = authorized(
PermissionType.TABLE,
PermissionLevel.READ
)
const ctx = {
appId,
// Create context
const ctx = createContext(this.app, socket, {
resourceId: tableId,
roleId: socket.data.roleId,
user: { _id: socket.data._id },
isAuthenticated: socket.data.isAuthenticated,
request: {
url: "/fake",
},
get: () => null,
throw: () => {
// If they don't have access, immediately disconnect them
socket.disconnect(true)
},
}
await context.doInAppContext(appId, async () => {
await middleware(ctx, async () => {
appId,
})
// Construct full middleware chain to assess permissions
const middlewares = [
userAgent,
auth.buildAuthMiddleware([], {
publicAllowed: true,
}),
currentApp,
authorized(PermissionType.TABLE, PermissionLevel.READ),
]
// Run all koa middlewares
try {
await runMiddlewares(ctx, middlewares, async () => {
// Middlewares are finished and we have permission
// Join room for this resource
const room = `${appId}-${tableId}`
await this.joinRoom(socket, room)
@ -55,7 +57,9 @@ export default class GridSocket extends BaseSocket {
const sessions = await this.getRoomSessions(room)
callback({ users: sessions })
})
})
} catch (error) {
socket.disconnect(true)
}
}
)

View file

@ -0,0 +1,85 @@
import { Socket } from "socket.io"
import Cookies from "cookies"
import http from "http"
import Koa from "koa"
import { Header } from "@budibase/backend-core"
/**
* Constructs a fake Koa context to use for manually running middlewares in
* sockets
* @param app the Koa app
* @param socket the socket.io socket instance
* @param options additional metadata to populate the context with
*/
export const createContext = (
app: Koa,
socket: Socket,
options?: WebsocketContextOptions
) => {
const res = new http.ServerResponse(socket.request)
const context: WebsocketContext = {
...app.createContext(socket.request, res),
// Additional overrides needed to make our middlewares work with this
// fake koa context
resourceId: options?.resourceId,
path: "/fake",
request: {
url: "/fake",
headers: {
[Header.APP_ID]: options?.appId,
},
},
cookies: new Cookies(socket.request, res),
get: (field: string) => socket.request.headers?.[field] as string,
throw: (...params: any[]) => {
// Throw has a bunch of different signatures, so we'll just stringify
// whatever params we get given
throw new Error(
...(params?.join(" ") || "Unknown error in socket middleware")
)
},
// Needed for koa-useragent middleware
headers: socket.request.headers,
header: socket.request.headers,
}
return context
}
/**
* Runs a list of middlewares, nesting each callback inside each other mimic
* how the real middlewares run and ensuring that app or tenant context work
* as expected
* @param ctx the Koa context
* @param middlewares the array of middlewares to run
* @param callback a final callback for when all middlewares are completed
*/
export const runMiddlewares = async (
ctx: any,
middlewares: any[],
callback: Function
) => {
if (!middlewares[0]) {
await callback()
} else {
await middlewares[0](ctx, async () => {
await runMiddlewares(ctx, middlewares.slice(1), callback)
})
}
}
export interface WebsocketContext extends Omit<Koa.Context, "request"> {
request: {
url: string
headers: {
[key: string]: string | undefined
}
}
cookies: Cookies
}
export interface WebsocketContextOptions {
appId?: string
resourceId?: string
}

View file

@ -1,7 +1,6 @@
import { Server } from "socket.io"
import http from "http"
import Koa from "koa"
import Cookies from "cookies"
import { userAgent } from "koa-useragent"
import { auth, Header, redis } from "@budibase/backend-core"
import { createAdapter } from "@socket.io/redis-adapter"
@ -10,14 +9,17 @@ import { getSocketPubSubClients } from "../utilities/redis"
import { SocketEvent, SocketSessionTTL } from "@budibase/shared-core"
import { SocketSession } from "@budibase/types"
import { v4 as uuid } from "uuid"
import { createContext, runMiddlewares } from "./middleware"
const anonUser = () => ({
_id: uuid(),
email: "user@mail.com",
firstName: "Anonymous",
tenantId: "default",
})
export class BaseSocket {
app: Koa
io: Server
path: string
redisClient?: redis.Client
@ -28,6 +30,7 @@ export class BaseSocket {
path: string = "/",
additionalMiddlewares?: any[]
) {
this.app = app
this.path = path
this.io = new Server(server, {
path,
@ -45,52 +48,25 @@ export class BaseSocket {
// Apply middlewares
this.io.use(async (socket, next) => {
// Build fake koa context
const res = new http.ServerResponse(socket.request)
const ctx: any = {
...app.createContext(socket.request, res),
const ctx = createContext(this.app, socket)
// Additional overrides needed to make our middlewares work with this
// fake koa context
cookies: new Cookies(socket.request, res),
get: (field: string) => socket.request.headers[field],
throw: (code: number, message: string) => {
throw new Error(message)
},
// Needed for koa-useragent middleware
headers: socket.request.headers,
header: socket.request.headers,
// We don't really care about the path since it will never contain
// an app ID
path: "/socket",
}
// Run all koa middlewares
try {
for (let [idx, middleware] of middlewares.entries()) {
await middleware(ctx, () => {
if (idx === middlewares.length - 1) {
// Middlewares are finished
// Extract some data from our enriched koa context to persist
// as metadata for the socket
const user = ctx.user?._id ? ctx.user : anonUser()
const { _id, email, firstName, lastName } = user
socket.data = {
_id,
email,
firstName,
lastName,
sessionId: socket.id,
connectedAt: Date.now(),
isAuthenticated: ctx.isAuthenticated,
roleId: ctx.roleId,
}
next()
}
})
}
await runMiddlewares(ctx, middlewares, () => {
// Middlewares are finished
// Extract some data from our enriched koa context to persist
// as metadata for the socket
const user = ctx.user?._id ? ctx.user : anonUser()
const { _id, email, firstName, lastName } = user
socket.data = {
_id,
email,
firstName,
lastName,
sessionId: socket.id,
connectedAt: Date.now(),
}
next()
})
} catch (error: any) {
next(error)
}