redis.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. import { Redis as IoRedis, RedisOptions } from 'ioredis'
  2. import { exists } from '@server/helpers/custom-validators/misc.js'
  3. import { sha256 } from '@peertube/peertube-node-utils'
  4. import { logger } from '../helpers/logger.js'
  5. import { generateRandomString } from '../helpers/utils.js'
  6. import { CONFIG } from '../initializers/config.js'
  7. import {
  8. AP_CLEANER,
  9. CONTACT_FORM_LIFETIME,
  10. EMAIL_VERIFY_LIFETIME,
  11. RESUMABLE_UPLOAD_SESSION_LIFETIME,
  12. TWO_FACTOR_AUTH_REQUEST_TOKEN_LIFETIME,
  13. USER_PASSWORD_CREATE_LIFETIME,
  14. USER_PASSWORD_RESET_LIFETIME,
  15. VIEW_LIFETIME,
  16. WEBSERVER
  17. } from '../initializers/constants.js'
  18. class Redis {
  19. private static instance: Redis
  20. private initialized = false
  21. private connected = false
  22. private client: IoRedis
  23. private prefix: string
  24. private constructor () {
  25. }
  26. init () {
  27. // Already initialized
  28. if (this.initialized === true) return
  29. this.initialized = true
  30. const redisMode = CONFIG.REDIS.SENTINEL.ENABLED ? 'sentinel' : 'standalone'
  31. logger.info('Connecting to redis ' + redisMode + '...')
  32. this.client = new IoRedis(Redis.getRedisClientOptions('', { enableAutoPipelining: true }))
  33. this.client.on('error', err => logger.error('Redis failed to connect', { err }))
  34. this.client.on('connect', () => {
  35. logger.info('Connected to redis.')
  36. this.connected = true
  37. })
  38. this.client.on('reconnecting', (ms) => {
  39. logger.error(`Reconnecting to redis in ${ms}.`)
  40. })
  41. this.client.on('close', () => {
  42. logger.error('Connection to redis has closed.')
  43. this.connected = false
  44. })
  45. this.client.on('end', () => {
  46. logger.error('Connection to redis has closed and no more reconnects will be done.')
  47. })
  48. this.prefix = 'redis-' + WEBSERVER.HOST + '-'
  49. }
  50. static getRedisClientOptions (name?: string, options: RedisOptions = {}): RedisOptions {
  51. const connectionName = [ 'PeerTube', name ].join('')
  52. const connectTimeout = 20000 // Could be slow since node use sync call to compile PeerTube
  53. if (CONFIG.REDIS.SENTINEL.ENABLED) {
  54. return {
  55. connectionName,
  56. connectTimeout,
  57. enableTLSForSentinelMode: CONFIG.REDIS.SENTINEL.ENABLE_TLS,
  58. sentinelPassword: CONFIG.REDIS.AUTH,
  59. sentinels: CONFIG.REDIS.SENTINEL.SENTINELS,
  60. name: CONFIG.REDIS.SENTINEL.MASTER_NAME,
  61. ...options
  62. }
  63. }
  64. return {
  65. connectionName,
  66. connectTimeout,
  67. password: CONFIG.REDIS.AUTH,
  68. db: CONFIG.REDIS.DB,
  69. host: CONFIG.REDIS.HOSTNAME,
  70. port: CONFIG.REDIS.PORT,
  71. path: CONFIG.REDIS.SOCKET,
  72. showFriendlyErrorStack: true,
  73. ...options
  74. }
  75. }
  76. getClient () {
  77. return this.client
  78. }
  79. getPrefix () {
  80. return this.prefix
  81. }
  82. isConnected () {
  83. return this.connected
  84. }
  85. /* ************ Forgot password ************ */
  86. async setResetPasswordVerificationString (userId: number) {
  87. const generatedString = await generateRandomString(32)
  88. await this.setValue(this.generateResetPasswordKey(userId), generatedString, USER_PASSWORD_RESET_LIFETIME)
  89. return generatedString
  90. }
  91. async setCreatePasswordVerificationString (userId: number) {
  92. const generatedString = await generateRandomString(32)
  93. await this.setValue(this.generateResetPasswordKey(userId), generatedString, USER_PASSWORD_CREATE_LIFETIME)
  94. return generatedString
  95. }
  96. async removePasswordVerificationString (userId: number) {
  97. return this.removeValue(this.generateResetPasswordKey(userId))
  98. }
  99. async getResetPasswordVerificationString (userId: number) {
  100. return this.getValue(this.generateResetPasswordKey(userId))
  101. }
  102. /* ************ Two factor auth request ************ */
  103. async setTwoFactorRequest (userId: number, otpSecret: string) {
  104. const requestToken = await generateRandomString(32)
  105. await this.setValue(this.generateTwoFactorRequestKey(userId, requestToken), otpSecret, TWO_FACTOR_AUTH_REQUEST_TOKEN_LIFETIME)
  106. return requestToken
  107. }
  108. async getTwoFactorRequestToken (userId: number, requestToken: string) {
  109. return this.getValue(this.generateTwoFactorRequestKey(userId, requestToken))
  110. }
  111. /* ************ Email verification ************ */
  112. async setUserVerifyEmailVerificationString (userId: number) {
  113. const generatedString = await generateRandomString(32)
  114. await this.setValue(this.generateUserVerifyEmailKey(userId), generatedString, EMAIL_VERIFY_LIFETIME)
  115. return generatedString
  116. }
  117. async getUserVerifyEmailLink (userId: number) {
  118. return this.getValue(this.generateUserVerifyEmailKey(userId))
  119. }
  120. async setRegistrationVerifyEmailVerificationString (registrationId: number) {
  121. const generatedString = await generateRandomString(32)
  122. await this.setValue(this.generateRegistrationVerifyEmailKey(registrationId), generatedString, EMAIL_VERIFY_LIFETIME)
  123. return generatedString
  124. }
  125. async getRegistrationVerifyEmailLink (registrationId: number) {
  126. return this.getValue(this.generateRegistrationVerifyEmailKey(registrationId))
  127. }
  128. /* ************ Contact form per IP ************ */
  129. async setContactFormIp (ip: string) {
  130. return this.setValue(this.generateContactFormKey(ip), '1', CONTACT_FORM_LIFETIME)
  131. }
  132. async doesContactFormIpExist (ip: string) {
  133. return this.exists(this.generateContactFormKey(ip))
  134. }
  135. /* ************ Views per IP ************ */
  136. setSessionIdVideoView (ip: string, videoUUID: string) {
  137. return this.setValue(this.generateSessionIdViewKey(ip, videoUUID), '1', VIEW_LIFETIME.VIEW)
  138. }
  139. async doesVideoSessionIdViewExist (sessionId: string, videoUUID: string) {
  140. return this.exists(this.generateSessionIdViewKey(sessionId, videoUUID))
  141. }
  142. /* ************ Video views stats ************ */
  143. addVideoViewStats (videoId: number) {
  144. const { videoKey, setKey } = this.generateVideoViewStatsKeys({ videoId })
  145. return Promise.all([
  146. this.addToSet(setKey, videoId.toString()),
  147. this.increment(videoKey)
  148. ])
  149. }
  150. async getVideoViewsStats (videoId: number, hour: number) {
  151. const { videoKey } = this.generateVideoViewStatsKeys({ videoId, hour })
  152. const valueString = await this.getValue(videoKey)
  153. const valueInt = parseInt(valueString, 10)
  154. if (isNaN(valueInt)) {
  155. logger.error('Cannot get videos views stats of video %d in hour %d: views number is NaN (%s).', videoId, hour, valueString)
  156. return undefined
  157. }
  158. return valueInt
  159. }
  160. async listVideosViewedForStats (hour: number) {
  161. const { setKey } = this.generateVideoViewStatsKeys({ hour })
  162. const stringIds = await this.getSet(setKey)
  163. return stringIds.map(s => parseInt(s, 10))
  164. }
  165. deleteVideoViewsStats (videoId: number, hour: number) {
  166. const { setKey, videoKey } = this.generateVideoViewStatsKeys({ videoId, hour })
  167. return Promise.all([
  168. this.deleteFromSet(setKey, videoId.toString()),
  169. this.deleteKey(videoKey)
  170. ])
  171. }
  172. /* ************ Local video views buffer ************ */
  173. addLocalVideoView (videoId: number) {
  174. const { videoKey, setKey } = this.generateLocalVideoViewsKeys(videoId)
  175. return Promise.all([
  176. this.addToSet(setKey, videoId.toString()),
  177. this.increment(videoKey)
  178. ])
  179. }
  180. async getLocalVideoViews (videoId: number) {
  181. const { videoKey } = this.generateLocalVideoViewsKeys(videoId)
  182. const valueString = await this.getValue(videoKey)
  183. const valueInt = parseInt(valueString, 10)
  184. if (isNaN(valueInt)) {
  185. logger.error('Cannot get videos views of video %d: views number is NaN (%s).', videoId, valueString)
  186. return undefined
  187. }
  188. return valueInt
  189. }
  190. async listLocalVideosViewed () {
  191. const { setKey } = this.generateLocalVideoViewsKeys()
  192. const stringIds = await this.getSet(setKey)
  193. return stringIds.map(s => parseInt(s, 10))
  194. }
  195. deleteLocalVideoViews (videoId: number) {
  196. const { setKey, videoKey } = this.generateLocalVideoViewsKeys(videoId)
  197. return Promise.all([
  198. this.deleteFromSet(setKey, videoId.toString()),
  199. this.deleteKey(videoKey)
  200. ])
  201. }
  202. /* ************ Video viewers stats ************ */
  203. getLocalVideoViewer (options: {
  204. key?: string
  205. // Or
  206. ip?: string
  207. videoId?: number
  208. }) {
  209. if (options.key) return this.getObject(options.key)
  210. const { viewerKey } = this.generateLocalVideoViewerKeys(options.ip, options.videoId)
  211. return this.getObject(viewerKey)
  212. }
  213. setLocalVideoViewer (sessionId: string, videoId: number, object: any) {
  214. const { setKey, viewerKey } = this.generateLocalVideoViewerKeys(sessionId, videoId)
  215. return Promise.all([
  216. this.addToSet(setKey, viewerKey),
  217. this.setObject(viewerKey, object)
  218. ])
  219. }
  220. listLocalVideoViewerKeys () {
  221. const { setKey } = this.generateLocalVideoViewerKeys()
  222. return this.getSet(setKey)
  223. }
  224. deleteLocalVideoViewersKeys (key: string) {
  225. const { setKey } = this.generateLocalVideoViewerKeys()
  226. return Promise.all([
  227. this.deleteFromSet(setKey, key),
  228. this.deleteKey(key)
  229. ])
  230. }
  231. /* ************ Resumable uploads final responses ************ */
  232. setUploadSession (uploadId: string) {
  233. return this.setValue('resumable-upload-' + uploadId, '', RESUMABLE_UPLOAD_SESSION_LIFETIME)
  234. }
  235. doesUploadSessionExist (uploadId: string) {
  236. return this.exists('resumable-upload-' + uploadId)
  237. }
  238. deleteUploadSession (uploadId: string) {
  239. return this.deleteKey('resumable-upload-' + uploadId)
  240. }
  241. /* ************ AP resource unavailability ************ */
  242. async addAPUnavailability (url: string) {
  243. const key = this.generateAPUnavailabilityKey(url)
  244. const value = await this.increment(key)
  245. await this.setExpiration(key, AP_CLEANER.PERIOD * 2)
  246. return value
  247. }
  248. /* ************ Keys generation ************ */
  249. private generateLocalVideoViewsKeys (videoId: number): { setKey: string, videoKey: string }
  250. private generateLocalVideoViewsKeys (): { setKey: string }
  251. private generateLocalVideoViewsKeys (videoId?: number) {
  252. return { setKey: `local-video-views-buffer`, videoKey: `local-video-views-buffer-${videoId}` }
  253. }
  254. generateLocalVideoViewerKeys (sessionId: string, videoId: number): { setKey: string, viewerKey: string }
  255. generateLocalVideoViewerKeys (): { setKey: string }
  256. generateLocalVideoViewerKeys (sessionId?: string, videoId?: number) {
  257. return {
  258. setKey: `local-video-viewer-stats-keys`,
  259. viewerKey: sessionId && videoId
  260. ? `local-video-viewer-stats-${sessionId}-${videoId}`
  261. : undefined
  262. }
  263. }
  264. private generateVideoViewStatsKeys (options: { videoId?: number, hour?: number }) {
  265. const hour = exists(options.hour)
  266. ? options.hour
  267. : new Date().getHours()
  268. return { setKey: `videos-view-h${hour}`, videoKey: `video-view-${options.videoId}-h${hour}` }
  269. }
  270. private generateResetPasswordKey (userId: number) {
  271. return 'reset-password-' + userId
  272. }
  273. private generateTwoFactorRequestKey (userId: number, token: string) {
  274. return 'two-factor-request-' + userId + '-' + token
  275. }
  276. private generateUserVerifyEmailKey (userId: number) {
  277. return 'verify-email-user-' + userId
  278. }
  279. private generateRegistrationVerifyEmailKey (registrationId: number) {
  280. return 'verify-email-registration-' + registrationId
  281. }
  282. generateSessionIdViewKey (sessionId: string, videoUUID: string) {
  283. return `views-${videoUUID}-${sessionId}`
  284. }
  285. private generateContactFormKey (ip: string) {
  286. return 'contact-form-' + sha256(CONFIG.SECRETS.PEERTUBE + '-' + ip)
  287. }
  288. private generateAPUnavailabilityKey (url: string) {
  289. return 'ap-unavailability-' + sha256(url)
  290. }
  291. /* ************ Redis helpers ************ */
  292. private getValue (key: string) {
  293. return this.client.get(this.prefix + key)
  294. }
  295. private getSet (key: string) {
  296. return this.client.smembers(this.prefix + key)
  297. }
  298. private addToSet (key: string, value: string) {
  299. return this.client.sadd(this.prefix + key, value)
  300. }
  301. private deleteFromSet (key: string, value: string) {
  302. return this.client.srem(this.prefix + key, value)
  303. }
  304. private deleteKey (key: string) {
  305. return this.client.del(this.prefix + key)
  306. }
  307. private async getObject (key: string) {
  308. const value = await this.getValue(key)
  309. if (!value) return null
  310. return JSON.parse(value)
  311. }
  312. private setObject (key: string, value: { [ id: string ]: number | string }, expirationMilliseconds?: number) {
  313. return this.setValue(key, JSON.stringify(value), expirationMilliseconds)
  314. }
  315. private async setValue (key: string, value: string, expirationMilliseconds?: number) {
  316. const result = expirationMilliseconds !== undefined
  317. ? await this.client.set(this.prefix + key, value, 'PX', expirationMilliseconds)
  318. : await this.client.set(this.prefix + key, value)
  319. if (result !== 'OK') throw new Error('Redis set result is not OK.')
  320. }
  321. private removeValue (key: string) {
  322. return this.client.del(this.prefix + key)
  323. }
  324. private increment (key: string) {
  325. return this.client.incr(this.prefix + key)
  326. }
  327. private async exists (key: string) {
  328. const result = await this.client.exists(this.prefix + key)
  329. return result !== 0
  330. }
  331. private setExpiration (key: string, ms: number) {
  332. return this.client.expire(this.prefix + key, ms / 1000)
  333. }
  334. static get Instance () {
  335. return this.instance || (this.instance = new this())
  336. }
  337. }
  338. // ---------------------------------------------------------------------------
  339. export {
  340. Redis
  341. }