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` 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: () => { getBasePluginClientPath: () => {
return '/p' return '/p'
}, },

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,7 @@
import express from 'express' import express from 'express'
import { createReadStream, createWriteStream } from 'fs' import { createReadStream, createWriteStream } from 'fs'
import { ensureDir, outputFile, readJSON } from 'fs-extra' import { ensureDir, outputFile, readJSON } from 'fs-extra'
import { Server } from 'http'
import { basename, join } from 'path' import { basename, join } from 'path'
import { decachePlugin } from '@server/helpers/decache' import { decachePlugin } from '@server/helpers/decache'
import { ApplicationModel } from '@server/models/application/application' import { ApplicationModel } from '@server/models/application/application'
@ -67,9 +68,37 @@ export class PluginManager implements ServerHook {
private hooks: { [name: string]: HookInformationValue[] } = {} private hooks: { [name: string]: HookInformationValue[] } = {}
private translations: PluginLocalesTranslations = {} private translations: PluginLocalesTranslations = {}
private server: Server
private constructor () { 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 ###################### // ###################### Getters ######################
isRegistered (npmName: string) { 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 { return {
registerStore: registerHelpers, registerStore: registerHelpers,

View file

@ -1,4 +1,5 @@
import express from 'express' import express from 'express'
import { Server } from 'http'
import { logger } from '@server/helpers/logger' import { logger } from '@server/helpers/logger'
import { onExternalUserAuthenticated } from '@server/lib/auth/external-auth' import { onExternalUserAuthenticated } from '@server/lib/auth/external-auth'
import { VideoConstantManagerFactory } from '@server/lib/plugins/video-constant-manager-factory' import { VideoConstantManagerFactory } from '@server/lib/plugins/video-constant-manager-factory'
@ -8,7 +9,8 @@ import {
RegisterServerAuthExternalResult, RegisterServerAuthExternalResult,
RegisterServerAuthPassOptions, RegisterServerAuthPassOptions,
RegisterServerExternalAuthenticatedResult, RegisterServerExternalAuthenticatedResult,
RegisterServerOptions RegisterServerOptions,
RegisterServerWebSocketRouteOptions
} from '@server/types/plugins' } from '@server/types/plugins'
import { import {
EncoderOptionsBuilder, EncoderOptionsBuilder,
@ -49,12 +51,15 @@ export class RegisterHelpers {
private readonly onSettingsChangeCallbacks: SettingsChangeCallback[] = [] private readonly onSettingsChangeCallbacks: SettingsChangeCallback[] = []
private readonly webSocketRoutes: RegisterServerWebSocketRouteOptions[] = []
private readonly router: express.Router private readonly router: express.Router
private readonly videoConstantManagerFactory: VideoConstantManagerFactory private readonly videoConstantManagerFactory: VideoConstantManagerFactory
constructor ( constructor (
private readonly npmName: string, private readonly npmName: string,
private readonly plugin: PluginModel, private readonly plugin: PluginModel,
private readonly server: Server,
private readonly onHookAdded: (options: RegisterServerHookOptions) => void private readonly onHookAdded: (options: RegisterServerHookOptions) => void
) { ) {
this.router = express.Router() this.router = express.Router()
@ -66,6 +71,7 @@ export class RegisterHelpers {
const registerSetting = this.buildRegisterSetting() const registerSetting = this.buildRegisterSetting()
const getRouter = this.buildGetRouter() const getRouter = this.buildGetRouter()
const registerWebSocketRoute = this.buildRegisterWebSocketRoute()
const settingsManager = this.buildSettingsManager() const settingsManager = this.buildSettingsManager()
const storageManager = this.buildStorageManager() const storageManager = this.buildStorageManager()
@ -85,13 +91,14 @@ export class RegisterHelpers {
const unregisterIdAndPassAuth = this.buildUnregisterIdAndPassAuth() const unregisterIdAndPassAuth = this.buildUnregisterIdAndPassAuth()
const unregisterExternalAuth = this.buildUnregisterExternalAuth() const unregisterExternalAuth = this.buildUnregisterExternalAuth()
const peertubeHelpers = buildPluginHelpers(this.plugin, this.npmName) const peertubeHelpers = buildPluginHelpers(this.server, this.plugin, this.npmName)
return { return {
registerHook, registerHook,
registerSetting, registerSetting,
getRouter, getRouter,
registerWebSocketRoute,
settingsManager, settingsManager,
storageManager, storageManager,
@ -180,10 +187,20 @@ export class RegisterHelpers {
return this.onSettingsChangeCallbacks return this.onSettingsChangeCallbacks
} }
getWebSocketRoutes () {
return this.webSocketRoutes
}
private buildGetRouter () { private buildGetRouter () {
return () => this.router return () => this.router
} }
private buildRegisterWebSocketRoute () {
return (options: RegisterServerWebSocketRouteOptions) => {
this.webSocketRoutes.push(options)
}
}
private buildRegisterSetting () { private buildRegisterSetting () {
return (options: RegisterServerSettingOptions) => { return (options: RegisterServerSettingOptions) => {
this.settings.push(options) 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-storage'
import './plugin-transcoding' import './plugin-transcoding'
import './plugin-unloading' import './plugin-unloading'
import './plugin-websocket'
import './translations' import './translations'
import './video-constants' 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 './plugin-library.model'
export * from './register-server-auth.model' export * from './register-server-auth.model'
export * from './register-server-option.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 { Response, Router } from 'express'
import { Server } from 'http'
import { Logger } from 'winston' import { Logger } from 'winston'
import { ActorModel } from '@server/models/actor/actor' import { ActorModel } from '@server/models/actor/actor'
import { import {
@ -22,6 +23,7 @@ import {
RegisterServerAuthExternalResult, RegisterServerAuthExternalResult,
RegisterServerAuthPassOptions RegisterServerAuthPassOptions
} from './register-server-auth.model' } from './register-server-auth.model'
import { RegisterServerWebSocketRouteOptions } from './register-server-websocket-route.model'
export type PeerTubeHelpers = { export type PeerTubeHelpers = {
logger: Logger logger: Logger
@ -83,6 +85,9 @@ export type PeerTubeHelpers = {
} }
server: { server: {
// PeerTube >= 5.0
getHTTPServer: () => Server
getServerActor: () => Promise<ActorModel> getServerActor: () => Promise<ActorModel>
} }
@ -97,6 +102,8 @@ export type PeerTubeHelpers = {
// PeerTube >= 3.2 // PeerTube >= 3.2
getBaseRouterRoute: () => string getBaseRouterRoute: () => string
// PeerTube >= 5.0
getBaseWebSocketRoute: () => string
// PeerTube >= 3.2 // PeerTube >= 3.2
getDataDirectoryPath: () => string getDataDirectoryPath: () => string
@ -140,5 +147,12 @@ export type RegisterServerOptions = {
// * /plugins/:pluginName/router/... // * /plugins/:pluginName/router/...
getRouter(): 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 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) - [Storage](#storage)
- [Update video constants](#update-video-constants) - [Update video constants](#update-video-constants)
- [Add custom routes](#add-custom-routes) - [Add custom routes](#add-custom-routes)
- [Add custom WebSocket handlers](#add-custom-websocket-handlers)
- [Add external auth methods](#add-external-auth-methods) - [Add external auth methods](#add-external-auth-methods)
- [Add new transcoding profiles](#add-new-transcoding-profiles) - [Add new transcoding profiles](#add-new-transcoding-profiles)
- [Server helpers](#server-helpers) - [Server helpers](#server-helpers)
@ -317,6 +318,41 @@ The `ping` route can be accessed using:
* Or `/plugins/:pluginName/router/ping` * 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 #### 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): 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; try_files /dev/null @api_websocket;
} }
# Plugin websocket routes
location ~ ^/plugins/[^/]+(/[^/]+)?/ws/ {
try_files /dev/null @api_websocket;
}
## ##
# Performance optimizations # Performance optimizations
# For extra performance please refer to https://github.com/denji/nginx-tuning # For extra performance please refer to https://github.com/denji/nginx-tuning