static.ts 8.0 KB

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