client.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. import express from 'express'
  2. import { constants, promises as fs } from 'fs'
  3. import { readFile } from 'fs-extra'
  4. import { join } from 'path'
  5. import { logger } from '@server/helpers/logger'
  6. import { CONFIG } from '@server/initializers/config'
  7. import { Hooks } from '@server/lib/plugins/hooks'
  8. import { HttpStatusCode } from '@shared/models'
  9. import { buildFileLocale, getCompleteLocale, is18nLocale, LOCALE_FILES } from '@shared/core-utils/i18n'
  10. import { root } from '../helpers/core-utils'
  11. import { STATIC_MAX_AGE } from '../initializers/constants'
  12. import { ClientHtml, sendHTML, serveIndexHTML } from '../lib/client-html'
  13. import { asyncMiddleware, embedCSP } from '../middlewares'
  14. const clientsRouter = express.Router()
  15. const distPath = join(root(), 'client', 'dist')
  16. const testEmbedPath = join(distPath, 'standalone', 'videos', 'test-embed.html')
  17. // Special route that add OpenGraph and oEmbed tags
  18. // Do not use a template engine for a so little thing
  19. clientsRouter.use([ '/w/p/:id', '/videos/watch/playlist/:id' ], asyncMiddleware(generateWatchPlaylistHtmlPage))
  20. clientsRouter.use([ '/w/:id', '/videos/watch/:id' ], asyncMiddleware(generateWatchHtmlPage))
  21. clientsRouter.use([ '/accounts/:nameWithHost', '/a/:nameWithHost' ], asyncMiddleware(generateAccountHtmlPage))
  22. clientsRouter.use([ '/video-channels/:nameWithHost', '/c/:nameWithHost' ], asyncMiddleware(generateVideoChannelHtmlPage))
  23. clientsRouter.use('/@:nameWithHost', asyncMiddleware(generateActorHtmlPage))
  24. const embedMiddlewares = [
  25. CONFIG.CSP.ENABLED
  26. ? embedCSP
  27. : (req: express.Request, res: express.Response, next: express.NextFunction) => next(),
  28. // Set headers
  29. (req: express.Request, res: express.Response, next: express.NextFunction) => {
  30. res.removeHeader('X-Frame-Options')
  31. // Don't cache HTML file since it's an index to the immutable JS/CSS files
  32. res.setHeader('Cache-Control', 'public, max-age=0')
  33. next()
  34. },
  35. asyncMiddleware(generateEmbedHtmlPage)
  36. ]
  37. clientsRouter.use('/videos/embed', ...embedMiddlewares)
  38. clientsRouter.use('/video-playlists/embed', ...embedMiddlewares)
  39. const testEmbedController = (req: express.Request, res: express.Response) => res.sendFile(testEmbedPath)
  40. clientsRouter.use('/videos/test-embed', testEmbedController)
  41. clientsRouter.use('/video-playlists/test-embed', testEmbedController)
  42. // Dynamic PWA manifest
  43. clientsRouter.get('/manifest.webmanifest', asyncMiddleware(generateManifest))
  44. // Static client overrides
  45. // Must be consistent with static client overrides redirections in /support/nginx/peertube
  46. const staticClientOverrides = [
  47. 'assets/images/logo.svg',
  48. 'assets/images/favicon.png',
  49. 'assets/images/icons/icon-36x36.png',
  50. 'assets/images/icons/icon-48x48.png',
  51. 'assets/images/icons/icon-72x72.png',
  52. 'assets/images/icons/icon-96x96.png',
  53. 'assets/images/icons/icon-144x144.png',
  54. 'assets/images/icons/icon-192x192.png',
  55. 'assets/images/icons/icon-512x512.png'
  56. ]
  57. for (const staticClientOverride of staticClientOverrides) {
  58. const overridePhysicalPath = join(CONFIG.STORAGE.CLIENT_OVERRIDES_DIR, staticClientOverride)
  59. clientsRouter.use(`/client/${staticClientOverride}`, asyncMiddleware(serveClientOverride(overridePhysicalPath)))
  60. }
  61. clientsRouter.use('/client/locales/:locale/:file.json', serveServerTranslations)
  62. clientsRouter.use('/client', express.static(distPath, { maxAge: STATIC_MAX_AGE.CLIENT }))
  63. // 404 for static files not found
  64. clientsRouter.use('/client/*', (req: express.Request, res: express.Response) => {
  65. res.status(HttpStatusCode.NOT_FOUND_404).end()
  66. })
  67. // Always serve index client page (the client is a single page application, let it handle routing)
  68. // Try to provide the right language index.html
  69. clientsRouter.use('/(:language)?', asyncMiddleware(serveIndexHTML))
  70. // ---------------------------------------------------------------------------
  71. export {
  72. clientsRouter
  73. }
  74. // ---------------------------------------------------------------------------
  75. function serveServerTranslations (req: express.Request, res: express.Response) {
  76. const locale = req.params.locale
  77. const file = req.params.file
  78. if (is18nLocale(locale) && LOCALE_FILES.includes(file)) {
  79. const completeLocale = getCompleteLocale(locale)
  80. const completeFileLocale = buildFileLocale(completeLocale)
  81. const path = join(__dirname, `../../../client/dist/locale/${file}.${completeFileLocale}.json`)
  82. return res.sendFile(path, { maxAge: STATIC_MAX_AGE.SERVER })
  83. }
  84. return res.status(HttpStatusCode.NOT_FOUND_404).end()
  85. }
  86. async function generateEmbedHtmlPage (req: express.Request, res: express.Response) {
  87. const hookName = req.originalUrl.startsWith('/video-playlists/')
  88. ? 'filter:html.embed.video-playlist.allowed.result'
  89. : 'filter:html.embed.video.allowed.result'
  90. const allowParameters = { req }
  91. const allowedResult = await Hooks.wrapFun(
  92. isEmbedAllowed,
  93. allowParameters,
  94. hookName
  95. )
  96. if (!allowedResult || allowedResult.allowed !== true) {
  97. logger.info('Embed is not allowed.', { allowedResult })
  98. return sendHTML(allowedResult?.html || '', res)
  99. }
  100. const html = await ClientHtml.getEmbedHTML()
  101. return sendHTML(html, res)
  102. }
  103. async function generateWatchHtmlPage (req: express.Request, res: express.Response) {
  104. const html = await ClientHtml.getWatchHTMLPage(req.params.id + '', req, res)
  105. return sendHTML(html, res)
  106. }
  107. async function generateWatchPlaylistHtmlPage (req: express.Request, res: express.Response) {
  108. const html = await ClientHtml.getWatchPlaylistHTMLPage(req.params.id + '', req, res)
  109. return sendHTML(html, res)
  110. }
  111. async function generateAccountHtmlPage (req: express.Request, res: express.Response) {
  112. const html = await ClientHtml.getAccountHTMLPage(req.params.nameWithHost, req, res)
  113. return sendHTML(html, res)
  114. }
  115. async function generateVideoChannelHtmlPage (req: express.Request, res: express.Response) {
  116. const html = await ClientHtml.getVideoChannelHTMLPage(req.params.nameWithHost, req, res)
  117. return sendHTML(html, res)
  118. }
  119. async function generateActorHtmlPage (req: express.Request, res: express.Response) {
  120. const html = await ClientHtml.getActorHTMLPage(req.params.nameWithHost, req, res)
  121. return sendHTML(html, res)
  122. }
  123. async function generateManifest (req: express.Request, res: express.Response) {
  124. const manifestPhysicalPath = join(root(), 'client', 'dist', 'manifest.webmanifest')
  125. const manifestJson = await readFile(manifestPhysicalPath, 'utf8')
  126. const manifest = JSON.parse(manifestJson)
  127. manifest.name = CONFIG.INSTANCE.NAME
  128. manifest.short_name = CONFIG.INSTANCE.NAME
  129. manifest.description = CONFIG.INSTANCE.SHORT_DESCRIPTION
  130. res.json(manifest)
  131. }
  132. function serveClientOverride (path: string) {
  133. return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
  134. try {
  135. await fs.access(path, constants.F_OK)
  136. // Serve override client
  137. res.sendFile(path, { maxAge: STATIC_MAX_AGE.SERVER })
  138. } catch {
  139. // Serve dist client
  140. next()
  141. }
  142. }
  143. }
  144. type AllowedResult = { allowed: boolean, html?: string }
  145. function isEmbedAllowed (_object: {
  146. req: express.Request
  147. }): AllowedResult {
  148. return { allowed: true }
  149. }