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'
},
return {
getBaseStaticRoute: unimplemented,
getBaseRouterRoute: unimplemented,
+ getBaseWebSocketRoute: unimplemented,
getBasePluginClientPath: unimplemented,
getSettings: () => {
getBaseRouterRoute: () => string
+ // PeerTube >= 5.0
+ getBaseWebSocketRoute: () => string
+
getBasePluginClientPath: () => string
isLoggedIn: () => boolean
GeoIPUpdateScheduler.Instance.enable()
OpenTelemetryMetrics.Instance.registerMetrics()
+ PluginManager.Instance.init(server)
+ // Before PeerTubeSocket init
+ PluginManager.Instance.registerWebSocketRouter()
+
PeerTubeSocket.Instance.init(server)
VideoViewsManager.Instance.init()
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'
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()
const config = buildConfigHelpers()
- const server = buildServerHelpers()
+ const server = buildServerHelpers(httpServer)
const moderation = buildModerationHelpers()
}
}
-function buildServerHelpers () {
+function buildServerHelpers (httpServer: Server) {
return {
+ getHTTPServer: () => httpServer,
+
getServerActor: () => getServerActor()
}
}
getBaseRouterRoute: () => `/plugins/${plugin.name}/${plugin.version}/router/`,
+ getBaseWebSocketRoute: () => `/plugins/${plugin.name}/${plugin.version}/ws/`,
+
getDataDirectoryPath: () => join(CONFIG.STORAGE.PLUGINS_DIR, 'data', npmName)
}
}
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'
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) {
})
}
- const registerHelpers = new RegisterHelpers(npmName, plugin, onHookAdded.bind(this))
+ const registerHelpers = new RegisterHelpers(npmName, plugin, this.server, onHookAdded.bind(this))
return {
registerStore: registerHelpers,
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'
RegisterServerAuthExternalResult,
RegisterServerAuthPassOptions,
RegisterServerExternalAuthenticatedResult,
- RegisterServerOptions
+ RegisterServerOptions,
+ RegisterServerWebSocketRouteOptions
} from '@server/types/plugins'
import {
EncoderOptionsBuilder,
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()
const registerSetting = this.buildRegisterSetting()
const getRouter = this.buildGetRouter()
+ const registerWebSocketRoute = this.buildRegisterWebSocketRoute()
const settingsManager = this.buildSettingsManager()
const storageManager = this.buildStorageManager()
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,
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)
--- /dev/null
+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
+}
+
+// ###########################################################################
--- /dev/null
+{
+ "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": {}
+}
import './plugin-storage'
import './plugin-transcoding'
import './plugin-unloading'
+import './plugin-websocket'
import './translations'
import './video-constants'
--- /dev/null
+/* 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 ])
+ })
+})
export * from './plugin-library.model'
export * from './register-server-auth.model'
export * from './register-server-option.model'
+export * from './register-server-websocket-route.model'
import { Response, Router } from 'express'
+import { Server } from 'http'
import { Logger } from 'winston'
import { ActorModel } from '@server/models/actor/actor'
import {
RegisterServerAuthExternalResult,
RegisterServerAuthPassOptions
} from './register-server-auth.model'
+import { RegisterServerWebSocketRouteOptions } from './register-server-websocket-route.model'
export type PeerTubeHelpers = {
logger: Logger
}
server: {
+ // PeerTube >= 5.0
+ getHTTPServer: () => Server
+
getServerActor: () => Promise<ActorModel>
}
// PeerTube >= 3.2
getBaseRouterRoute: () => string
+ // PeerTube >= 5.0
+ getBaseWebSocketRoute: () => string
// PeerTube >= 3.2
getDataDirectoryPath: () => string
// * /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
}
--- /dev/null
+import { IncomingMessage } from 'http'
+import { Duplex } from 'stream'
+
+export type RegisterServerWebSocketRouteOptions = {
+ route: string
+
+ handler: (request: IncomingMessage, socket: Duplex, head: Buffer) => any
+}
- [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)
* 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):
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