Add ability for plugins to register ws routes
This commit is contained in:
parent
9866921cbf
commit
9d4c60dccc
16 changed files with 262 additions and 7 deletions
|
@ -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'
|
||||
},
|
||||
|
|
|
@ -43,6 +43,7 @@ export class PeerTubePlugin {
|
|||
return {
|
||||
getBaseStaticRoute: unimplemented,
|
||||
getBaseRouterRoute: unimplemented,
|
||||
getBaseWebSocketRoute: unimplemented,
|
||||
getBasePluginClientPath: unimplemented,
|
||||
|
||||
getSettings: () => {
|
||||
|
|
|
@ -24,6 +24,9 @@ export type RegisterClientHelpers = {
|
|||
|
||||
getBaseRouterRoute: () => string
|
||||
|
||||
// PeerTube >= 5.0
|
||||
getBaseWebSocketRoute: () => string
|
||||
|
||||
getBasePluginClientPath: () => string
|
||||
|
||||
isLoggedIn: () => boolean
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
36
server/tests/fixtures/peertube-plugin-test-websocket/main.js
vendored
Normal file
36
server/tests/fixtures/peertube-plugin-test-websocket/main.js
vendored
Normal 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
|
||||
}
|
||||
|
||||
// ###########################################################################
|
20
server/tests/fixtures/peertube-plugin-test-websocket/package.json
vendored
Normal file
20
server/tests/fixtures/peertube-plugin-test-websocket/package.json
vendored
Normal 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": {}
|
||||
}
|
|
@ -8,5 +8,6 @@ import './plugin-router'
|
|||
import './plugin-storage'
|
||||
import './plugin-transcoding'
|
||||
import './plugin-unloading'
|
||||
import './plugin-websocket'
|
||||
import './translations'
|
||||
import './video-constants'
|
||||
|
|
70
server/tests/plugins/plugin-websocket.ts
Normal file
70
server/tests/plugins/plugin-websocket.ts
Normal 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 ])
|
||||
})
|
||||
})
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue