static.ts 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. import * as cors from 'cors'
  2. import * as express from 'express'
  3. import {
  4. HLS_STREAMING_PLAYLIST_DIRECTORY,
  5. ROUTE_CACHE_LIFETIME,
  6. STATIC_DOWNLOAD_PATHS,
  7. STATIC_MAX_AGE,
  8. STATIC_PATHS,
  9. WEBSERVER
  10. } from '../initializers/constants'
  11. import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache'
  12. import { cacheRoute } from '../middlewares/cache'
  13. import { asyncMiddleware, videosGetValidator } from '../middlewares'
  14. import { VideoModel } from '../models/video/video'
  15. import { UserModel } from '../models/account/user'
  16. import { VideoCommentModel } from '../models/video/video-comment'
  17. import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo'
  18. import { join } from 'path'
  19. import { root } from '../helpers/core-utils'
  20. import { CONFIG } from '../initializers/config'
  21. const packageJSON = require('../../../package.json')
  22. const staticRouter = express.Router()
  23. staticRouter.use(cors())
  24. /*
  25. Cors is very important to let other servers access torrent and video files
  26. */
  27. const torrentsPhysicalPath = CONFIG.STORAGE.TORRENTS_DIR
  28. staticRouter.use(
  29. STATIC_PATHS.TORRENTS,
  30. cors(),
  31. express.static(torrentsPhysicalPath, { maxAge: 0 }) // Don't cache because we could regenerate the torrent file
  32. )
  33. staticRouter.use(
  34. STATIC_DOWNLOAD_PATHS.TORRENTS + ':id-:resolution([0-9]+).torrent',
  35. asyncMiddleware(videosGetValidator),
  36. asyncMiddleware(downloadTorrent)
  37. )
  38. // Videos path for webseeding
  39. staticRouter.use(
  40. STATIC_PATHS.WEBSEED,
  41. cors(),
  42. express.static(CONFIG.STORAGE.VIDEOS_DIR, { fallthrough: false }) // 404 because we don't have this video
  43. )
  44. staticRouter.use(
  45. STATIC_PATHS.REDUNDANCY,
  46. cors(),
  47. express.static(CONFIG.STORAGE.REDUNDANCY_DIR, { fallthrough: false }) // 404 because we don't have this video
  48. )
  49. staticRouter.use(
  50. STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension',
  51. asyncMiddleware(videosGetValidator),
  52. asyncMiddleware(downloadVideoFile)
  53. )
  54. // HLS
  55. staticRouter.use(
  56. STATIC_PATHS.STREAMING_PLAYLISTS.HLS,
  57. cors(),
  58. express.static(HLS_STREAMING_PLAYLIST_DIRECTORY, { fallthrough: false }) // 404 if the file does not exist
  59. )
  60. // Thumbnails path for express
  61. const thumbnailsPhysicalPath = CONFIG.STORAGE.THUMBNAILS_DIR
  62. staticRouter.use(
  63. STATIC_PATHS.THUMBNAILS,
  64. express.static(thumbnailsPhysicalPath, { maxAge: STATIC_MAX_AGE, fallthrough: false }) // 404 if the file does not exist
  65. )
  66. const avatarsPhysicalPath = CONFIG.STORAGE.AVATARS_DIR
  67. staticRouter.use(
  68. STATIC_PATHS.AVATARS,
  69. express.static(avatarsPhysicalPath, { maxAge: STATIC_MAX_AGE, fallthrough: false }) // 404 if the file does not exist
  70. )
  71. // We don't have video previews, fetch them from the origin instance
  72. staticRouter.use(
  73. STATIC_PATHS.PREVIEWS + ':uuid.jpg',
  74. asyncMiddleware(getPreview)
  75. )
  76. // We don't have video captions, fetch them from the origin instance
  77. staticRouter.use(
  78. STATIC_PATHS.VIDEO_CAPTIONS + ':videoId-:captionLanguage([a-z]+).vtt',
  79. asyncMiddleware(getVideoCaption)
  80. )
  81. // robots.txt service
  82. staticRouter.get('/robots.txt',
  83. asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.ROBOTS)),
  84. (_, res: express.Response) => {
  85. res.type('text/plain')
  86. return res.send(CONFIG.INSTANCE.ROBOTS)
  87. }
  88. )
  89. // security.txt service
  90. staticRouter.get('/security.txt',
  91. (_, res: express.Response) => {
  92. return res.redirect(301, '/.well-known/security.txt')
  93. }
  94. )
  95. staticRouter.get('/.well-known/security.txt',
  96. asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.SECURITYTXT)),
  97. (_, res: express.Response) => {
  98. res.type('text/plain')
  99. return res.send(CONFIG.INSTANCE.SECURITYTXT + CONFIG.INSTANCE.SECURITYTXT_CONTACT)
  100. }
  101. )
  102. // nodeinfo service
  103. staticRouter.use('/.well-known/nodeinfo',
  104. asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.NODEINFO)),
  105. (_, res: express.Response) => {
  106. return res.json({
  107. links: [
  108. {
  109. rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0',
  110. href: WEBSERVER.URL + '/nodeinfo/2.0.json'
  111. }
  112. ]
  113. })
  114. }
  115. )
  116. staticRouter.use('/nodeinfo/:version.json',
  117. asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.NODEINFO)),
  118. asyncMiddleware(generateNodeinfo)
  119. )
  120. // dnt-policy.txt service (see https://www.eff.org/dnt-policy)
  121. staticRouter.use('/.well-known/dnt-policy.txt',
  122. asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.DNT_POLICY)),
  123. (_, res: express.Response) => {
  124. res.type('text/plain')
  125. return res.sendFile(join(root(), 'dist/server/static/dnt-policy/dnt-policy-1.0.txt'))
  126. }
  127. )
  128. // dnt service (see https://www.w3.org/TR/tracking-dnt/#status-resource)
  129. staticRouter.use('/.well-known/dnt/',
  130. (_, res: express.Response) => {
  131. res.json({ tracking: 'N' })
  132. }
  133. )
  134. staticRouter.use('/.well-known/change-password',
  135. (_, res: express.Response) => {
  136. res.redirect('/my-account/settings')
  137. }
  138. )
  139. staticRouter.use('/.well-known/host-meta',
  140. (_, res: express.Response) => {
  141. res.type('application/xml')
  142. const xml = '<?xml version="1.0" encoding="UTF-8"?>\n' +
  143. '<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">\n' +
  144. ` <Link rel="lrdd" type="application/xrd+xml" template="${WEBSERVER.URL}/.well-known/webfinger?resource={uri}"/>\n` +
  145. '</XRD>'
  146. res.send(xml).end()
  147. }
  148. )
  149. // ---------------------------------------------------------------------------
  150. export {
  151. staticRouter
  152. }
  153. // ---------------------------------------------------------------------------
  154. async function getPreview (req: express.Request, res: express.Response) {
  155. const result = await VideosPreviewCache.Instance.getFilePath(req.params.uuid)
  156. if (!result) return res.sendStatus(404)
  157. return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE })
  158. }
  159. async function getVideoCaption (req: express.Request, res: express.Response) {
  160. const result = await VideosCaptionCache.Instance.getFilePath({
  161. videoId: req.params.videoId,
  162. language: req.params.captionLanguage
  163. })
  164. if (!result) return res.sendStatus(404)
  165. return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE })
  166. }
  167. async function generateNodeinfo (req: express.Request, res: express.Response) {
  168. const { totalVideos } = await VideoModel.getStats()
  169. const { totalLocalVideoComments } = await VideoCommentModel.getStats()
  170. const { totalUsers } = await UserModel.getStats()
  171. let json = {}
  172. if (req.params.version && (req.params.version === '2.0')) {
  173. json = {
  174. version: '2.0',
  175. software: {
  176. name: 'peertube',
  177. version: packageJSON.version
  178. },
  179. protocols: [
  180. 'activitypub'
  181. ],
  182. services: {
  183. inbound: [],
  184. outbound: [
  185. 'atom1.0',
  186. 'rss2.0'
  187. ]
  188. },
  189. openRegistrations: CONFIG.SIGNUP.ENABLED,
  190. usage: {
  191. users: {
  192. total: totalUsers
  193. },
  194. localPosts: totalVideos,
  195. localComments: totalLocalVideoComments
  196. },
  197. metadata: {
  198. taxonomy: {
  199. postsName: 'Videos'
  200. },
  201. nodeName: CONFIG.INSTANCE.NAME,
  202. nodeDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION
  203. }
  204. } as HttpNodeinfoDiasporaSoftwareNsSchema20
  205. res.contentType('application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"')
  206. } else {
  207. json = { error: 'Nodeinfo schema version not handled' }
  208. res.status(404)
  209. }
  210. return res.send(json).end()
  211. }
  212. async function downloadTorrent (req: express.Request, res: express.Response, next: express.NextFunction) {
  213. const { video, videoFile } = getVideoAndFile(req, res)
  214. if (!videoFile) return res.status(404).end()
  215. return res.download(video.getTorrentFilePath(videoFile), `${video.name}-${videoFile.resolution}p.torrent`)
  216. }
  217. async function downloadVideoFile (req: express.Request, res: express.Response, next: express.NextFunction) {
  218. const { video, videoFile } = getVideoAndFile(req, res)
  219. if (!videoFile) return res.status(404).end()
  220. return res.download(video.getVideoFilePath(videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`)
  221. }
  222. function getVideoAndFile (req: express.Request, res: express.Response) {
  223. const resolution = parseInt(req.params.resolution, 10)
  224. const video = res.locals.video
  225. const videoFile = video.VideoFiles.find(f => f.resolution === resolution)
  226. return { video, videoFile }
  227. }