static.ts 13 KB

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