redis.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  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. setIPVideoView (ip: string, videoUUID: string) {
  137. return this.setValue(this.generateIPViewKey(ip, videoUUID), '1', VIEW_LIFETIME.VIEW)
  138. }
  139. async doesVideoIPViewExist (ip: string, videoUUID: string) {
  140. return this.exists(this.generateIPViewKey(ip, 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 (ip: string, videoId: number, object: any) {
  214. const { setKey, viewerKey } = this.generateLocalVideoViewerKeys(ip, 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 (ip: string, videoId: number): { setKey: string, viewerKey: string }
  255. generateLocalVideoViewerKeys (): { setKey: string }
  256. generateLocalVideoViewerKeys (ip?: string, videoId?: number) {
  257. const anonymousIP = sha256(CONFIG.SECRETS + '-' + ip)
  258. return { setKey: `local-video-viewer-stats-keys`, viewerKey: `local-video-viewer-stats-${anonymousIP}-${videoId}` }
  259. }
  260. private generateVideoViewStatsKeys (options: { videoId?: number, hour?: number }) {
  261. const hour = exists(options.hour)
  262. ? options.hour
  263. : new Date().getHours()
  264. return { setKey: `videos-view-h${hour}`, videoKey: `video-view-${options.videoId}-h${hour}` }
  265. }
  266. private generateResetPasswordKey (userId: number) {
  267. return 'reset-password-' + userId
  268. }
  269. private generateTwoFactorRequestKey (userId: number, token: string) {
  270. return 'two-factor-request-' + userId + '-' + token
  271. }
  272. private generateUserVerifyEmailKey (userId: number) {
  273. return 'verify-email-user-' + userId
  274. }
  275. private generateRegistrationVerifyEmailKey (registrationId: number) {
  276. return 'verify-email-registration-' + registrationId
  277. }
  278. generateIPViewKey (ip: string, videoUUID: string) {
  279. return `views-${videoUUID}-${sha256(CONFIG.SECRETS.PEERTUBE + '-' + ip)}`
  280. }
  281. private generateContactFormKey (ip: string) {
  282. return 'contact-form-' + sha256(CONFIG.SECRETS.PEERTUBE + '-' + ip)
  283. }
  284. private generateAPUnavailabilityKey (url: string) {
  285. return 'ap-unavailability-' + sha256(url)
  286. }
  287. /* ************ Redis helpers ************ */
  288. private getValue (key: string) {
  289. return this.client.get(this.prefix + key)
  290. }
  291. private getSet (key: string) {
  292. return this.client.smembers(this.prefix + key)
  293. }
  294. private addToSet (key: string, value: string) {
  295. return this.client.sadd(this.prefix + key, value)
  296. }
  297. private deleteFromSet (key: string, value: string) {
  298. return this.client.srem(this.prefix + key, value)
  299. }
  300. private deleteKey (key: string) {
  301. return this.client.del(this.prefix + key)
  302. }
  303. private async getObject (key: string) {
  304. const value = await this.getValue(key)
  305. if (!value) return null
  306. return JSON.parse(value)
  307. }
  308. private setObject (key: string, value: { [ id: string ]: number | string }, expirationMilliseconds?: number) {
  309. return this.setValue(key, JSON.stringify(value), expirationMilliseconds)
  310. }
  311. private async setValue (key: string, value: string, expirationMilliseconds?: number) {
  312. const result = expirationMilliseconds !== undefined
  313. ? await this.client.set(this.prefix + key, value, 'PX', expirationMilliseconds)
  314. : await this.client.set(this.prefix + key, value)
  315. if (result !== 'OK') throw new Error('Redis set result is not OK.')
  316. }
  317. private removeValue (key: string) {
  318. return this.client.del(this.prefix + key)
  319. }
  320. private increment (key: string) {
  321. return this.client.incr(this.prefix + key)
  322. }
  323. private async exists (key: string) {
  324. const result = await this.client.exists(this.prefix + key)
  325. return result !== 0
  326. }
  327. private setExpiration (key: string, ms: number) {
  328. return this.client.expire(this.prefix + key, ms / 1000)
  329. }
  330. static get Instance () {
  331. return this.instance || (this.instance = new this())
  332. }
  333. }
  334. // ---------------------------------------------------------------------------
  335. export {
  336. Redis
  337. }