1
0
Fork 0

Add ability for plugins to register ws routes

This commit is contained in:
Chocobozzz 2022-10-11 11:07:40 +02:00
parent 9866921cbf
commit 9d4c60dccc
No known key found for this signature in database
GPG key ID: 583A612D890159BE
16 changed files with 262 additions and 7 deletions

View file

@ -202,6 +202,11 @@ export class PluginService implements ClientHook {
return environment.apiUrl + `${pathPrefix}/${plugin.name}/${plugin.version}/router`
},
getBaseWebSocketRoute: () => {
const pathPrefix = PluginsManager.getPluginPathPrefix(pluginInfo.isTheme)
return environment.apiUrl + `${pathPrefix}/${plugin.name}/${plugin.version}/ws`
},
getBasePluginClientPath: () => {
return '/p'
},

View file

@ -43,6 +43,7 @@ export class PeerTubePlugin {
return {
getBaseStaticRoute: unimplemented,
getBaseRouterRoute: unimplemented,
getBaseWebSocketRoute: unimplemented,
getBasePluginClientPath: unimplemented,
getSettings: () => {

View file

@ -24,6 +24,9 @@ export type RegisterClientHelpers = {
getBaseRouterRoute: () => string
// PeerTube >= 5.0
getBaseWebSocketRoute: () => string
getBasePluginClientPath: () => string
isLoggedIn: () => boolean

View file

@ -328,6 +328,10 @@ async function startApplication () {
GeoIPUpdateScheduler.Instance.enable()
OpenTelemetryMetrics.Instance.registerMetrics()
PluginManager.Instance.init(server)
// Before PeerTubeSocket init
PluginManager.Instance.registerWebSocketRouter()
PeerTubeSocket.Instance.init(server)
VideoViewsManager.Instance.init()

View file

@ -1,4 +1,5 @@
import express from 'express'
import { Server } from 'http'
import { join } from 'path'
import { ffprobePromise } from '@server/helpers/ffmpeg/ffprobe-utils'
import { buildLogger } from '@server/helpers/logger'
@ -17,12 +18,12 @@ import { MPlugin, MVideo, UserNotificationModelForApi } from '@server/types/mode
import { PeerTubeHelpers } from '@server/types/plugins'
import { VideoBlacklistCreate, VideoStorage } from '@shared/models'
import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../blocklist'
import { PeerTubeSocket } from '../peertube-socket'
import { ServerConfigManager } from '../server-config-manager'
import { blacklistVideo, unblacklistVideo } from '../video-blacklist'
import { VideoPathManager } from '../video-path-manager'
import { PeerTubeSocket } from '../peertube-socket'
function buildPluginHelpers (pluginModel: MPlugin, npmName: string): PeerTubeHelpers {
function buildPluginHelpers (httpServer: Server, pluginModel: MPlugin, npmName: string): PeerTubeHelpers {
const logger = buildPluginLogger(npmName)
const database = buildDatabaseHelpers()
@ -30,7 +31,7 @@ function buildPluginHelpers (pluginModel: MPlugin, npmName: string): PeerTubeHel
const config = buildConfigHelpers()
const server = buildServerHelpers()
const server = buildServerHelpers(httpServer)
const moderation = buildModerationHelpers()
@ -69,8 +70,10 @@ function buildDatabaseHelpers () {
}
}
function buildServerHelpers () {
function buildServerHelpers (httpServer: Server) {
return {
getHTTPServer: () => httpServer,
getServerActor: () => getServerActor()
}
}
@ -218,6 +221,8 @@ function buildPluginRelatedHelpers (plugin: MPlugin, npmName: string) {
getBaseRouterRoute: () => `/plugins/${plugin.name}/${plugin.version}/router/`,
getBaseWebSocketRoute: () => `/plugins/${plugin.name}/${plugin.version}/ws/`,
getDataDirectoryPath: () => join(CONFIG.STORAGE.PLUGINS_DIR, 'data', npmName)
}
}

View file

@ -1,6 +1,7 @@
import express from 'express'
import { createReadStream, createWriteStream } from 'fs'
import { ensureDir, outputFile, readJSON } from 'fs-extra'
import { Server } from 'http'
import { basename, join } from 'path'
import { decachePlugin } from '@server/helpers/decache'
import { ApplicationModel } from '@server/models/application/application'
@ -67,9 +68,37 @@ export class PluginManager implements ServerHook {
private hooks: { [name: string]: HookInformationValue[] } = {}
private translations: PluginLocalesTranslations = {}
private server: Server
private constructor () {
}
init (server: Server) {
this.server = server
}
registerWebSocketRouter () {
this.server.on('upgrade', (request, socket, head) => {
const url = request.url
const matched = url.match(`/plugins/([^/]+)/([^/]+/)?ws(/.*)`)
if (!matched) return
const npmName = PluginModel.buildNpmName(matched[1], PluginType.PLUGIN)
const subRoute = matched[3]
const result = this.getRegisteredPluginOrTheme(npmName)
if (!result) return
const routes = result.registerHelpers.getWebSocketRoutes()
const wss = routes.find(r => r.route.startsWith(subRoute))
if (!wss) return
wss.handler(request, socket, head)
})
}
// ###################### Getters ######################
isRegistered (npmName: string) {
@ -581,7 +610,7 @@ export class PluginManager implements ServerHook {
})
}
const registerHelpers = new RegisterHelpers(npmName, plugin, onHookAdded.bind(this))
const registerHelpers = new RegisterHelpers(npmName, plugin, this.server, onHookAdded.bind(this))
return {
registerStore: registerHelpers,

View file

@ -1,4 +1,5 @@
import express from 'express'
import { Server } from 'http'
import { logger } from '@server/helpers/logger'
import { onExternalUserAuthenticated } from '@server/lib/auth/external-auth'
import { VideoConstantManagerFactory } from '@server/lib/plugins/video-constant-manager-factory'
@ -8,7 +9,8 @@ import {
RegisterServerAuthExternalResult,
RegisterServerAuthPassOptions,
RegisterServerExternalAuthenticatedResult,
RegisterServerOptions
RegisterServerOptions,
RegisterServerWebSocketRouteOptions
} from '@server/types/plugins'
import {
EncoderOptionsBuilder,
@ -49,12 +51,15 @@ export class RegisterHelpers {
private readonly onSettingsChangeCallbacks: SettingsChangeCallback[] = []
private readonly webSocketRoutes: RegisterServerWebSocketRouteOptions[] = []
private readonly router: express.Router
private readonly videoConstantManagerFactory: VideoConstantManagerFactory
constructor (
private readonly npmName: string,
private readonly plugin: PluginModel,
private readonly server: Server,
private readonly onHookAdded: (options: RegisterServerHookOptions) => void
) {
this.router = express.Router()
@ -66,6 +71,7 @@ export class RegisterHelpers {
const registerSetting = this.buildRegisterSetting()
const getRouter = this.buildGetRouter()
const registerWebSocketRoute = this.buildRegisterWebSocketRoute()
const settingsManager = this.buildSettingsManager()
const storageManager = this.buildStorageManager()
@ -85,13 +91,14 @@ export class RegisterHelpers {
const unregisterIdAndPassAuth = this.buildUnregisterIdAndPassAuth()
const unregisterExternalAuth = this.buildUnregisterExternalAuth()
const peertubeHelpers = buildPluginHelpers(this.plugin, this.npmName)
const peertubeHelpers = buildPluginHelpers(this.server, this.plugin, this.npmName)
return {
registerHook,
registerSetting,
getRouter,
registerWebSocketRoute,
settingsManager,
storageManager,
@ -180,10 +187,20 @@ export class RegisterHelpers {
return this.onSettingsChangeCallbacks
}
getWebSocketRoutes () {
return this.webSocketRoutes
}
private buildGetRouter () {
return () => this.router
}
private buildRegisterWebSocketRoute () {
return (options: RegisterServerWebSocketRouteOptions) => {
this.webSocketRoutes.push(options)
}
}
private buildRegisterSetting () {
return (options: RegisterServerSettingOptions) => {
this.settings.push(options)

View file

@ -0,0 +1,36 @@
const WebSocketServer = require('ws').WebSocketServer
async function register ({
registerWebSocketRoute
}) {
const wss = new WebSocketServer({ noServer: true })
wss.on('connection', function connection(ws) {
ws.on('message', function message(data) {
if (data.toString() === 'ping') {
ws.send('pong')
}
})
})
registerWebSocketRoute({
route: '/toto',
handler: (request, socket, head) => {
wss.handleUpgrade(request, socket, head, ws => {
wss.emit('connection', ws, request)
})
}
})
}
async function unregister () {
return
}
module.exports = {
register,
unregister
}
// ###########################################################################

View file

@ -0,0 +1,20 @@
{
"name": "peertube-plugin-test-websocket",
"version": "0.0.1",
"description": "Plugin test websocket",
"engine": {
"peertube": ">=1.3.0"
},
"keywords": [
"peertube",
"plugin"
],
"homepage": "https://github.com/Chocobozzz/PeerTube",
"author": "Chocobozzz",
"bugs": "https://github.com/Chocobozzz/PeerTube/issues",
"library": "./main.js",
"staticDirs": {},
"css": [],
"clientScripts": [],
"translations": {}
}

View file

@ -8,5 +8,6 @@ import './plugin-router'
import './plugin-storage'
import './plugin-transcoding'
import './plugin-unloading'
import './plugin-websocket'
import './translations'
import './video-constants'

View file

@ -0,0 +1,70 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import WebSocket from 'ws'
import { cleanupTests, createSingleServer, PeerTubeServer, PluginsCommand, setAccessTokensToServers } from '@shared/server-commands'
function buildWebSocket (server: PeerTubeServer, path: string) {
return new WebSocket('ws://' + server.host + path)
}
function expectErrorOrTimeout (server: PeerTubeServer, path: string, expectedTimeout: number) {
return new Promise<void>((res, rej) => {
const ws = buildWebSocket(server, path)
ws.on('error', () => res())
const timeout = setTimeout(() => res(), expectedTimeout)
ws.on('open', () => {
clearTimeout(timeout)
return rej(new Error('Connect did not timeout'))
})
})
}
describe('Test plugin websocket', function () {
let server: PeerTubeServer
const basePaths = [
'/plugins/test-websocket/ws/',
'/plugins/test-websocket/0.0.1/ws/'
]
before(async function () {
this.timeout(30000)
server = await createSingleServer(1)
await setAccessTokensToServers([ server ])
await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-websocket') })
})
it('Should not connect to the websocket without the appropriate path', async function () {
const paths = [
'/plugins/unknown/ws/',
'/plugins/unknown/0.0.1/ws/'
]
for (const path of paths) {
await expectErrorOrTimeout(server, path, 1000)
}
})
it('Should not connect to the websocket without the appropriate sub path', async function () {
for (const path of basePaths) {
await expectErrorOrTimeout(server, path + '/unknown', 1000)
}
})
it('Should connect to the websocket and receive pong', function (done) {
const ws = buildWebSocket(server, basePaths[0])
ws.on('open', () => ws.send('ping'))
ws.on('message', data => {
if (data.toString() === 'pong') return done()
})
})
after(async function () {
await cleanupTests([ server ])
})
})

View file

@ -1,3 +1,4 @@
export * from './plugin-library.model'
export * from './register-server-auth.model'
export * from './register-server-option.model'
export * from './register-server-websocket-route.model'

View file

@ -1,4 +1,5 @@
import { Response, Router } from 'express'
import { Server } from 'http'
import { Logger } from 'winston'
import { ActorModel } from '@server/models/actor/actor'
import {
@ -22,6 +23,7 @@ import {
RegisterServerAuthExternalResult,
RegisterServerAuthPassOptions
} from './register-server-auth.model'
import { RegisterServerWebSocketRouteOptions } from './register-server-websocket-route.model'
export type PeerTubeHelpers = {
logger: Logger
@ -83,6 +85,9 @@ export type PeerTubeHelpers = {
}
server: {
// PeerTube >= 5.0
getHTTPServer: () => Server
getServerActor: () => Promise<ActorModel>
}
@ -97,6 +102,8 @@ export type PeerTubeHelpers = {
// PeerTube >= 3.2
getBaseRouterRoute: () => string
// PeerTube >= 5.0
getBaseWebSocketRoute: () => string
// PeerTube >= 3.2
getDataDirectoryPath: () => string
@ -140,5 +147,12 @@ export type RegisterServerOptions = {
// * /plugins/:pluginName/router/...
getRouter(): Router
// PeerTube >= 5.0
// Register WebSocket route
// Base routes of the WebSocket router are
// * /plugins/:pluginName/:pluginVersion/ws/...
// * /plugins/:pluginName/ws/...
registerWebSocketRoute: (options: RegisterServerWebSocketRouteOptions) => void
peertubeHelpers: PeerTubeHelpers
}

View file

@ -0,0 +1,8 @@
import { IncomingMessage } from 'http'
import { Duplex } from 'stream'
export type RegisterServerWebSocketRouteOptions = {
route: string
handler: (request: IncomingMessage, socket: Duplex, head: Buffer) => any
}

View file

@ -12,6 +12,7 @@
- [Storage](#storage)
- [Update video constants](#update-video-constants)
- [Add custom routes](#add-custom-routes)
- [Add custom WebSocket handlers](#add-custom-websocket-handlers)
- [Add external auth methods](#add-external-auth-methods)
- [Add new transcoding profiles](#add-new-transcoding-profiles)
- [Server helpers](#server-helpers)
@ -317,6 +318,41 @@ The `ping` route can be accessed using:
* Or `/plugins/:pluginName/router/ping`
#### Add custom WebSocket handlers
You can create custom WebSocket servers (like [ws](https://github.com/websockets/ws) for example) using `registerWebSocketRoute`:
```js
function register ({
registerWebSocketRoute,
peertubeHelpers
}) {
const wss = new WebSocketServer({ noServer: true })
wss.on('connection', function connection(ws) {
peertubeHelpers.logger.info('WebSocket connected!')
setInterval(() => {
ws.send('WebSocket message sent by server');
}, 1000)
})
registerWebSocketRoute({
route: '/my-websocket-route',
handler: (request, socket, head) => {
wss.handleUpgrade(request, socket, head, ws => {
wss.emit('connection', ws, request)
})
}
})
}
```
The `my-websocket-route` route can be accessed using:
* `/plugins/:pluginName/:pluginVersion/ws/my-websocket-route`
* Or `/plugins/:pluginName/ws/my-websocket-route`
#### Add external auth methods
If you want to add a classic username/email and password auth method (like [LDAP](https://framagit.org/framasoft/peertube/official-plugins/-/tree/master/peertube-plugin-auth-ldap) for example):

View file

@ -132,6 +132,11 @@ server {
try_files /dev/null @api_websocket;
}
# Plugin websocket routes
location ~ ^/plugins/[^/]+(/[^/]+)?/ws/ {
try_files /dev/null @api_websocket;
}
##
# Performance optimizations
# For extra performance please refer to https://github.com/denji/nginx-tuning