static.ts 9.0 KB

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