static.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. import * as cors from 'cors'
  2. import * as express from 'express'
  3. import {
  4. CONSTRAINTS_FIELDS,
  5. DEFAULT_THEME_NAME,
  6. HLS_STREAMING_PLAYLIST_DIRECTORY,
  7. PEERTUBE_VERSION,
  8. ROUTE_CACHE_LIFETIME,
  9. STATIC_DOWNLOAD_PATHS,
  10. STATIC_MAX_AGE,
  11. STATIC_PATHS,
  12. WEBSERVER
  13. } from '../initializers/constants'
  14. import { cacheRoute } from '../middlewares/cache'
  15. import { asyncMiddleware, videosDownloadValidator } from '../middlewares'
  16. import { VideoModel } from '../models/video/video'
  17. import { UserModel } from '../models/account/user'
  18. import { VideoCommentModel } from '../models/video/video-comment'
  19. import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo'
  20. import { join } from 'path'
  21. import { root } from '../helpers/core-utils'
  22. import { CONFIG, isEmailEnabled } from '../initializers/config'
  23. import { getPreview, getVideoCaption } from './lazy-static'
  24. import { VideoStreamingPlaylistType } from '@shared/models/videos/video-streaming-playlist.type'
  25. import { MVideoFile, MVideoFullLight } from '@server/types/models'
  26. import { getTorrentFilePath, getVideoFilePath } from '@server/lib/video-paths'
  27. import { getThemeOrDefault } from '../lib/plugins/theme-utils'
  28. import { getEnabledResolutions, getRegisteredPlugins, getRegisteredThemes } from '@server/controllers/api/config'
  29. const staticRouter = express.Router()
  30. staticRouter.use(cors())
  31. /*
  32. Cors is very important to let other servers access torrent and video files
  33. */
  34. const torrentsPhysicalPath = CONFIG.STORAGE.TORRENTS_DIR
  35. staticRouter.use(
  36. STATIC_PATHS.TORRENTS,
  37. cors(),
  38. express.static(torrentsPhysicalPath, { maxAge: 0 }) // Don't cache because we could regenerate the torrent file
  39. )
  40. staticRouter.use(
  41. STATIC_DOWNLOAD_PATHS.TORRENTS + ':id-:resolution([0-9]+).torrent',
  42. asyncMiddleware(videosDownloadValidator),
  43. downloadTorrent
  44. )
  45. staticRouter.use(
  46. STATIC_DOWNLOAD_PATHS.TORRENTS + ':id-:resolution([0-9]+)-hls.torrent',
  47. asyncMiddleware(videosDownloadValidator),
  48. downloadHLSVideoFileTorrent
  49. )
  50. // Videos path for webseeding
  51. staticRouter.use(
  52. STATIC_PATHS.WEBSEED,
  53. cors(),
  54. express.static(CONFIG.STORAGE.VIDEOS_DIR, { fallthrough: false }) // 404 because we don't have this video
  55. )
  56. staticRouter.use(
  57. STATIC_PATHS.REDUNDANCY,
  58. cors(),
  59. express.static(CONFIG.STORAGE.REDUNDANCY_DIR, { fallthrough: false }) // 404 because we don't have this video
  60. )
  61. staticRouter.use(
  62. STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension',
  63. asyncMiddleware(videosDownloadValidator),
  64. downloadVideoFile
  65. )
  66. staticRouter.use(
  67. STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+)-fragmented.:extension',
  68. asyncMiddleware(videosDownloadValidator),
  69. downloadHLSVideoFile
  70. )
  71. // HLS
  72. staticRouter.use(
  73. STATIC_PATHS.STREAMING_PLAYLISTS.HLS,
  74. cors(),
  75. express.static(HLS_STREAMING_PLAYLIST_DIRECTORY, { fallthrough: false }) // 404 if the file does not exist
  76. )
  77. // Thumbnails path for express
  78. const thumbnailsPhysicalPath = CONFIG.STORAGE.THUMBNAILS_DIR
  79. staticRouter.use(
  80. STATIC_PATHS.THUMBNAILS,
  81. express.static(thumbnailsPhysicalPath, { maxAge: STATIC_MAX_AGE.SERVER, fallthrough: false }) // 404 if the file does not exist
  82. )
  83. // DEPRECATED: use lazy-static route instead
  84. const avatarsPhysicalPath = CONFIG.STORAGE.AVATARS_DIR
  85. staticRouter.use(
  86. STATIC_PATHS.AVATARS,
  87. express.static(avatarsPhysicalPath, { maxAge: STATIC_MAX_AGE.SERVER, fallthrough: false }) // 404 if the file does not exist
  88. )
  89. // DEPRECATED: use lazy-static route instead
  90. staticRouter.use(
  91. STATIC_PATHS.PREVIEWS + ':uuid.jpg',
  92. asyncMiddleware(getPreview)
  93. )
  94. // DEPRECATED: use lazy-static route instead
  95. staticRouter.use(
  96. STATIC_PATHS.VIDEO_CAPTIONS + ':videoId-:captionLanguage([a-z]+).vtt',
  97. asyncMiddleware(getVideoCaption)
  98. )
  99. // robots.txt service
  100. staticRouter.get('/robots.txt',
  101. asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.ROBOTS)),
  102. (_, res: express.Response) => {
  103. res.type('text/plain')
  104. return res.send(CONFIG.INSTANCE.ROBOTS)
  105. }
  106. )
  107. // security.txt service
  108. staticRouter.get('/security.txt',
  109. (_, res: express.Response) => {
  110. return res.redirect(301, '/.well-known/security.txt')
  111. }
  112. )
  113. staticRouter.get('/.well-known/security.txt',
  114. asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.SECURITYTXT)),
  115. (_, res: express.Response) => {
  116. res.type('text/plain')
  117. return res.send(CONFIG.INSTANCE.SECURITYTXT + CONFIG.INSTANCE.SECURITYTXT_CONTACT)
  118. }
  119. )
  120. // nodeinfo service
  121. staticRouter.use('/.well-known/nodeinfo',
  122. asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.NODEINFO)),
  123. (_, res: express.Response) => {
  124. return res.json({
  125. links: [
  126. {
  127. rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0',
  128. href: WEBSERVER.URL + '/nodeinfo/2.0.json'
  129. }
  130. ]
  131. })
  132. }
  133. )
  134. staticRouter.use('/nodeinfo/:version.json',
  135. asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.NODEINFO)),
  136. asyncMiddleware(generateNodeinfo)
  137. )
  138. // dnt-policy.txt service (see https://www.eff.org/dnt-policy)
  139. staticRouter.use('/.well-known/dnt-policy.txt',
  140. asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.DNT_POLICY)),
  141. (_, res: express.Response) => {
  142. res.type('text/plain')
  143. return res.sendFile(join(root(), 'dist/server/static/dnt-policy/dnt-policy-1.0.txt'))
  144. }
  145. )
  146. // dnt service (see https://www.w3.org/TR/tracking-dnt/#status-resource)
  147. staticRouter.use('/.well-known/dnt/',
  148. (_, res: express.Response) => {
  149. res.json({ tracking: 'N' })
  150. }
  151. )
  152. staticRouter.use('/.well-known/change-password',
  153. (_, res: express.Response) => {
  154. res.redirect('/my-account/settings')
  155. }
  156. )
  157. staticRouter.use('/.well-known/host-meta',
  158. (_, res: express.Response) => {
  159. res.type('application/xml')
  160. const xml = '<?xml version="1.0" encoding="UTF-8"?>\n' +
  161. '<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">\n' +
  162. ` <Link rel="lrdd" type="application/xrd+xml" template="${WEBSERVER.URL}/.well-known/webfinger?resource={uri}"/>\n` +
  163. '</XRD>'
  164. res.send(xml).end()
  165. }
  166. )
  167. // ---------------------------------------------------------------------------
  168. export {
  169. staticRouter
  170. }
  171. // ---------------------------------------------------------------------------
  172. async function generateNodeinfo (req: express.Request, res: express.Response) {
  173. const { totalVideos } = await VideoModel.getStats()
  174. const { totalLocalVideoComments } = await VideoCommentModel.getStats()
  175. const { totalUsers } = await UserModel.getStats()
  176. let json = {}
  177. if (req.params.version && (req.params.version === '2.0')) {
  178. json = {
  179. version: '2.0',
  180. software: {
  181. name: 'peertube',
  182. version: PEERTUBE_VERSION
  183. },
  184. protocols: [
  185. 'activitypub'
  186. ],
  187. services: {
  188. inbound: [],
  189. outbound: [
  190. 'atom1.0',
  191. 'rss2.0'
  192. ]
  193. },
  194. openRegistrations: CONFIG.SIGNUP.ENABLED,
  195. usage: {
  196. users: {
  197. total: totalUsers
  198. },
  199. localPosts: totalVideos,
  200. localComments: totalLocalVideoComments
  201. },
  202. metadata: {
  203. taxonomy: {
  204. postsName: 'Videos'
  205. },
  206. nodeName: CONFIG.INSTANCE.NAME,
  207. nodeDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
  208. nodeConfig: {
  209. search: {
  210. remoteUri: {
  211. users: CONFIG.SEARCH.REMOTE_URI.USERS,
  212. anonymous: CONFIG.SEARCH.REMOTE_URI.ANONYMOUS
  213. }
  214. },
  215. plugin: {
  216. registered: getRegisteredPlugins()
  217. },
  218. theme: {
  219. registered: getRegisteredThemes(),
  220. default: getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME)
  221. },
  222. email: {
  223. enabled: isEmailEnabled()
  224. },
  225. contactForm: {
  226. enabled: CONFIG.CONTACT_FORM.ENABLED
  227. },
  228. transcoding: {
  229. hls: {
  230. enabled: CONFIG.TRANSCODING.HLS.ENABLED
  231. },
  232. webtorrent: {
  233. enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED
  234. },
  235. enabledResolutions: getEnabledResolutions()
  236. },
  237. import: {
  238. videos: {
  239. http: {
  240. enabled: CONFIG.IMPORT.VIDEOS.HTTP.ENABLED
  241. },
  242. torrent: {
  243. enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED
  244. }
  245. }
  246. },
  247. autoBlacklist: {
  248. videos: {
  249. ofUsers: {
  250. enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED
  251. }
  252. }
  253. },
  254. avatar: {
  255. file: {
  256. size: {
  257. max: CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max
  258. },
  259. extensions: CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME
  260. }
  261. },
  262. video: {
  263. image: {
  264. extensions: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME,
  265. size: {
  266. max: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max
  267. }
  268. },
  269. file: {
  270. extensions: CONSTRAINTS_FIELDS.VIDEOS.EXTNAME
  271. }
  272. },
  273. videoCaption: {
  274. file: {
  275. size: {
  276. max: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max
  277. },
  278. extensions: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.EXTNAME
  279. }
  280. },
  281. user: {
  282. videoQuota: CONFIG.USER.VIDEO_QUOTA,
  283. videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY
  284. },
  285. trending: {
  286. videos: {
  287. intervalDays: CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
  288. }
  289. },
  290. tracker: {
  291. enabled: CONFIG.TRACKER.ENABLED
  292. }
  293. }
  294. }
  295. } as HttpNodeinfoDiasporaSoftwareNsSchema20
  296. res.contentType('application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"')
  297. } else {
  298. json = { error: 'Nodeinfo schema version not handled' }
  299. res.status(404)
  300. }
  301. return res.send(json).end()
  302. }
  303. function downloadTorrent (req: express.Request, res: express.Response) {
  304. const video = res.locals.videoAll
  305. const videoFile = getVideoFile(req, video.VideoFiles)
  306. if (!videoFile) return res.status(404).end()
  307. return res.download(getTorrentFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p.torrent`)
  308. }
  309. function downloadHLSVideoFileTorrent (req: express.Request, res: express.Response) {
  310. const video = res.locals.videoAll
  311. const playlist = getHLSPlaylist(video)
  312. if (!playlist) return res.status(404).end
  313. const videoFile = getVideoFile(req, playlist.VideoFiles)
  314. if (!videoFile) return res.status(404).end()
  315. return res.download(getTorrentFilePath(playlist, videoFile), `${video.name}-${videoFile.resolution}p-hls.torrent`)
  316. }
  317. function downloadVideoFile (req: express.Request, res: express.Response) {
  318. const video = res.locals.videoAll
  319. const videoFile = getVideoFile(req, video.VideoFiles)
  320. if (!videoFile) return res.status(404).end()
  321. return res.download(getVideoFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`)
  322. }
  323. function downloadHLSVideoFile (req: express.Request, res: express.Response) {
  324. const video = res.locals.videoAll
  325. const playlist = getHLSPlaylist(video)
  326. if (!playlist) return res.status(404).end
  327. const videoFile = getVideoFile(req, playlist.VideoFiles)
  328. if (!videoFile) return res.status(404).end()
  329. const filename = `${video.name}-${videoFile.resolution}p-${playlist.getStringType()}${videoFile.extname}`
  330. return res.download(getVideoFilePath(playlist, videoFile), filename)
  331. }
  332. function getVideoFile (req: express.Request, files: MVideoFile[]) {
  333. const resolution = parseInt(req.params.resolution, 10)
  334. return files.find(f => f.resolution === resolution)
  335. }
  336. function getHLSPlaylist (video: MVideoFullLight) {
  337. const playlist = video.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
  338. if (!playlist) return undefined
  339. return Object.assign(playlist, { Video: video })
  340. }