function segmentValidatorFactory (segmentsSha256Url: string) {
const segmentsJSON = fetchSha256Segments(segmentsSha256Url)
+ const regex = /bytes=(\d+)-(\d+)/
return async function segmentValidator (segment: Segment) {
- const segmentName = basename(segment.url)
+ const filename = basename(segment.url)
+ const captured = regex.exec(segment.range)
- const hashShouldBe = (await segmentsJSON)[segmentName]
+ const range = captured[1] + '-' + captured[2]
+
+ const hashShouldBe = (await segmentsJSON)[filename][range]
if (hashShouldBe === undefined) {
- throw new Error(`Unknown segment name ${segmentName} in segment validator`)
+ throw new Error(`Unknown segment name ${filename}/${range} in segment validator`)
}
const calculatedSha = bufferToEx(await sha256(segment.data))
if (calculatedSha !== hashShouldBe) {
- throw new Error(`Hashes does not correspond for segment ${segmentName} (expected: ${hashShouldBe} instead of ${calculatedSha})`)
+ throw new Error(
+ `Hashes does not correspond for segment ${filename}/${range}` +
+ `(expected: ${hashShouldBe} instead of ${calculatedSha})`
+ )
}
}
}
"fluent-ffmpeg": "^2.1.0",
"fs-extra": "^7.0.0",
"helmet": "^3.12.1",
- "hlsdownloader": "https://github.com/Chocobozzz/hlsdownloader#build",
"http-signature": "^1.2.0",
"ip-anonymize": "^0.0.6",
"ipaddr.js": "1.8.1",
}
function get (url: string, headers: any = {}) {
- return doRequest({
+ return doRequest<any>({
uri: url,
json: true,
headers: Object.assign(headers, {
resolution: VideoResolution
isPortraitMode?: boolean
- generateHlsPlaylist?: boolean
+ hlsPlaylist?: {
+ videoFilename: string
+ }
}
function transcode (options: TranscodeOptions) {
command = command.withFPS(fps)
}
- if (options.generateHlsPlaylist) {
- const segmentFilename = `${dirname(options.outputPath)}/${options.resolution}_%03d.ts`
+ if (options.hlsPlaylist) {
+ const videoPath = `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}`
command = command.outputOption('-hls_time 4')
.outputOption('-hls_list_size 0')
.outputOption('-hls_playlist_type vod')
- .outputOption('-hls_segment_filename ' + segmentFilename)
+ .outputOption('-hls_segment_filename ' + videoPath)
+ .outputOption('-hls_segment_type fmp4')
.outputOption('-f hls')
+ .outputOption('-hls_flags single_file')
}
command
function doRequest <T> (
requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean }
-): Bluebird<{ response: request.RequestResponse, body: any }> {
+): Bluebird<{ response: request.RequestResponse, body: T }> {
if (requestOptions.activityPub === true) {
if (!Array.isArray(requestOptions.headers)) requestOptions.headers = {}
requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER
import { Instance as ParseTorrent } from 'parse-torrent'
import { remove } from 'fs-extra'
import * as memoizee from 'memoizee'
-import { isArray } from './custom-validators/misc'
function deleteFileAsync (path: string) {
remove(path)
logger.info('Fetching remote actor %s.', actorUrl)
- const requestResult = await doRequest(options)
+ const requestResult = await doRequest<ActivityPubActor>(options)
normalizeActor(requestResult.body)
- const actorJSON: ActivityPubActor = requestResult.body
+ const actorJSON = requestResult.body
if (isActorObjectValid(actorJSON) === false) {
logger.debug('Remote actor JSON is not valid.', { actorJSON })
return { result: undefined, statusCode: requestResult.response.statusCode }
import { VideoModel } from '../models/video/video'
-import { basename, dirname, join } from 'path'
-import { HLS_PLAYLIST_DIRECTORY, CONFIG } from '../initializers'
-import { outputJSON, pathExists, readdir, readFile, remove, writeFile, move } from 'fs-extra'
+import { basename, join, dirname } from 'path'
+import { CONFIG, HLS_PLAYLIST_DIRECTORY } from '../initializers'
+import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra'
import { getVideoFileSize } from '../helpers/ffmpeg-utils'
import { sha256 } from '../helpers/core-utils'
import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
-import HLSDownloader from 'hlsdownloader'
import { logger } from '../helpers/logger'
-import { parse } from 'url'
+import { doRequest, doRequestAndSaveToFile } from '../helpers/requests'
+import { generateRandomString } from '../helpers/utils'
+import { flatten, uniq } from 'lodash'
async function updateMasterHLSPlaylist (video: VideoModel) {
const directory = join(HLS_PLAYLIST_DIRECTORY, video.uuid)
}
async function updateSha256Segments (video: VideoModel) {
- const directory = join(HLS_PLAYLIST_DIRECTORY, video.uuid)
- const files = await readdir(directory)
- const json: { [filename: string]: string} = {}
+ const json: { [filename: string]: { [range: string]: string } } = {}
+
+ const playlistDirectory = join(HLS_PLAYLIST_DIRECTORY, video.uuid)
+
+ // For all the resolutions available for this video
+ for (const file of video.VideoFiles) {
+ const rangeHashes: { [range: string]: string } = {}
+
+ const videoPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, file.resolution))
+ const playlistPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution))
- for (const file of files) {
- if (file.endsWith('.ts') === false) continue
+ // Maybe the playlist is not generated for this resolution yet
+ if (!await pathExists(playlistPath)) continue
- const buffer = await readFile(join(directory, file))
- const filename = basename(file)
+ const playlistContent = await readFile(playlistPath)
+ const ranges = getRangesFromPlaylist(playlistContent.toString())
- json[filename] = sha256(buffer)
+ const fd = await open(videoPath, 'r')
+ for (const range of ranges) {
+ const buf = Buffer.alloc(range.length)
+ await read(fd, buf, 0, range.length, range.offset)
+
+ rangeHashes[`${range.offset}-${range.offset + range.length - 1}`] = sha256(buf)
+ }
+ await close(fd)
+
+ const videoFilename = VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, file.resolution)
+ json[videoFilename] = rangeHashes
}
- const outputPath = join(directory, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename())
+ const outputPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename())
await outputJSON(outputPath, json)
}
-function downloadPlaylistSegments (playlistUrl: string, destinationDir: string, timeout: number) {
- let timer
+function getRangesFromPlaylist (playlistContent: string) {
+ const ranges: { offset: number, length: number }[] = []
+ const lines = playlistContent.split('\n')
+ const regex = /^#EXT-X-BYTERANGE:(\d+)@(\d+)$/
- logger.info('Importing HLS playlist %s', playlistUrl)
+ for (const line of lines) {
+ const captured = regex.exec(line)
- const params = {
- playlistURL: playlistUrl,
- destination: CONFIG.STORAGE.TMP_DIR
+ if (captured) {
+ ranges.push({ length: parseInt(captured[1], 10), offset: parseInt(captured[2], 10) })
+ }
}
- const downloader = new HLSDownloader(params)
-
- const hlsDestinationDir = join(CONFIG.STORAGE.TMP_DIR, dirname(parse(playlistUrl).pathname))
- return new Promise<string>(async (res, rej) => {
- downloader.startDownload(err => {
- clearTimeout(timer)
+ return ranges
+}
- if (err) {
- deleteTmpDirectory(hlsDestinationDir)
+function downloadPlaylistSegments (playlistUrl: string, destinationDir: string, timeout: number) {
+ let timer
- return rej(err)
- }
+ logger.info('Importing HLS playlist %s', playlistUrl)
- move(hlsDestinationDir, destinationDir, { overwrite: true })
- .then(() => res())
- .catch(err => {
- deleteTmpDirectory(hlsDestinationDir)
+ return new Promise<string>(async (res, rej) => {
+ const tmpDirectory = join(CONFIG.STORAGE.TMP_DIR, await generateRandomString(10))
- return rej(err)
- })
- })
+ await ensureDir(tmpDirectory)
timer = setTimeout(() => {
- deleteTmpDirectory(hlsDestinationDir)
+ deleteTmpDirectory(tmpDirectory)
return rej(new Error('HLS download timeout.'))
}, timeout)
- function deleteTmpDirectory (directory: string) {
- remove(directory)
- .catch(err => logger.error('Cannot delete path on HLS download error.', { err }))
+ try {
+ // Fetch master playlist
+ const subPlaylistUrls = await fetchUniqUrls(playlistUrl)
+
+ const subRequests = subPlaylistUrls.map(u => fetchUniqUrls(u))
+ const fileUrls = uniq(flatten(await Promise.all(subRequests)))
+
+ logger.debug('Will download %d HLS files.', fileUrls.length, { fileUrls })
+
+ for (const fileUrl of fileUrls) {
+ const destPath = join(tmpDirectory, basename(fileUrl))
+
+ await doRequestAndSaveToFile({ uri: fileUrl }, destPath)
+ }
+
+ clearTimeout(timer)
+
+ await move(tmpDirectory, destinationDir, { overwrite: true })
+
+ return res()
+ } catch (err) {
+ deleteTmpDirectory(tmpDirectory)
+
+ return rej(err)
}
})
+
+ function deleteTmpDirectory (directory: string) {
+ remove(directory)
+ .catch(err => logger.error('Cannot delete path on HLS download error.', { err }))
+ }
+
+ async function fetchUniqUrls (playlistUrl: string) {
+ const { body } = await doRequest<string>({ uri: playlistUrl })
+
+ if (!body) return []
+
+ const urls = body.split('\n')
+ .filter(line => line.endsWith('.m3u8') || line.endsWith('.mp4'))
+ .map(url => {
+ if (url.startsWith('http://') || url.startsWith('https://')) return url
+
+ return `${dirname(playlistUrl)}/${url}`
+ })
+
+ return uniq(urls)
+ }
}
// ---------------------------------------------------------------------------
outputPath,
resolution,
isPortraitMode,
- generateHlsPlaylist: true
+
+ hlsPlaylist: {
+ videoFilename: VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, resolution)
+ }
}
await transcode(transcodeOptions)
return 'segments-sha256.json'
}
+ static getHlsVideoName (uuid: string, resolution: number) {
+ return `${uuid}-${resolution}-fragmented.mp4`
+ }
+
static getHlsMasterPlaylistStaticPath (videoUUID: string) {
return join(STATIC_PATHS.PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename())
}
viewVideo,
wait,
waitUntilLog,
- checkVideoFilesWereRemoved, removeVideo, getVideoWithToken, reRunServer
+ checkVideoFilesWereRemoved, removeVideo, getVideoWithToken, reRunServer, checkSegmentHash
} from '../../../../shared/utils'
import { waitJobs } from '../../../../shared/utils/server/jobs'
expect(redundancy.baseUrl).to.equal(servers[0].url + '/static/redundancy/hls/' + videoUUID)
}
- await makeGetRequest({
- url: servers[0].url,
- statusCodeExpected: 200,
- path: `/static/redundancy/hls/${videoUUID}/360_000.ts`,
- contentType: null
- })
+ const baseUrlPlaylist = servers[1].url + '/static/playlists/hls'
+ const baseUrlSegment = servers[0].url + '/static/redundancy/hls'
+
+ const res = await getVideo(servers[0].url, videoUUID)
+ const hlsPlaylist = (res.body as VideoDetails).streamingPlaylists[0]
+
+ for (const resolution of [ 240, 360, 480, 720 ]) {
+ await checkSegmentHash(baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist)
+ }
for (const directory of [ 'test1/redundancy/hls', 'test2/playlists/hls' ]) {
const files = await readdir(join(root(), directory, videoUUID))
expect(files).to.have.length.at.least(4)
for (const resolution of [ 240, 360, 480, 720 ]) {
- expect(files.find(f => f === `${resolution}_000.ts`)).to.not.be.undefined
- expect(files.find(f => f === `${resolution}_001.ts`)).to.not.be.undefined
+ const filename = `${videoUUID}-${resolution}-fragmented.mp4`
+
+ expect(files.find(f => f === filename)).to.not.be.undefined
}
}
}
import 'mocha'
import {
checkDirectoryIsEmpty,
+ checkSegmentHash,
checkTmpIsEmpty,
doubleFollow,
flushAndRunMultipleServers,
flushTests,
getPlaylist,
- getSegment,
- getSegmentSha256,
getVideo,
killallServers,
removeVideo,
} from '../../../../shared/utils'
import { VideoDetails } from '../../../../shared/models/videos'
import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type'
-import { sha256 } from '../../../helpers/core-utils'
import { join } from 'path'
const expect = chai.expect
const res2 = await getPlaylist(`http://localhost:9001/static/playlists/hls/${videoUUID}/${resolution}.m3u8`)
const subPlaylist = res2.text
- expect(subPlaylist).to.contain(resolution + '_000.ts')
+ expect(subPlaylist).to.contain(`${videoUUID}-${resolution}-fragmented.mp4`)
}
}
{
- for (const resolution of resolutions) {
-
- const res2 = await getSegment(`http://localhost:9001/static/playlists/hls/${videoUUID}/${resolution}_000.ts`)
+ const baseUrl = 'http://localhost:9001/static/playlists/hls'
- const resSha = await getSegmentSha256(hlsPlaylist.segmentsSha256Url)
-
- const sha256Server = resSha.body[ resolution + '_000.ts' ]
- expect(sha256(res2.body)).to.equal(sha256Server)
+ for (const resolution of resolutions) {
+ await checkSegmentHash(baseUrl, baseUrl, videoUUID, resolution, hlsPlaylist)
}
}
}
'@context': string[]
type: 'OrderedCollection' | 'OrderedCollectionPage'
totalItems: number
- partOf?: string
orderedItems: T[]
+
+ partOf?: string
+ next?: string
+ first?: string
}
import { isAbsolute, join } from 'path'
import { parse } from 'url'
-function makeRawRequest (url: string, statusCodeExpected?: number) {
+function makeRawRequest (url: string, statusCodeExpected?: number, range?: string) {
const { host, protocol, pathname } = parse(url)
- return makeGetRequest({ url: `${protocol}//${host}`, path: pathname, statusCodeExpected })
+ return makeGetRequest({ url: `${protocol}//${host}`, path: pathname, statusCodeExpected, range })
}
function makeGetRequest (options: {
query?: any,
token?: string,
statusCodeExpected?: number,
- contentType?: string
+ contentType?: string,
+ range?: string
}) {
if (!options.statusCodeExpected) options.statusCodeExpected = 400
if (options.contentType === undefined) options.contentType = 'application/json'
if (options.contentType) req.set('Accept', options.contentType)
if (options.token) req.set('Authorization', 'Bearer ' + options.token)
if (options.query) req.query(options.query)
+ if (options.range) req.set('Range', options.range)
return req.expect(options.statusCodeExpected)
}
import { makeRawRequest } from '../requests/requests'
+import { sha256 } from '../../../server/helpers/core-utils'
+import { VideoStreamingPlaylist } from '../../models/videos/video-streaming-playlist.model'
+import { expect } from 'chai'
function getPlaylist (url: string, statusCodeExpected = 200) {
return makeRawRequest(url, statusCodeExpected)
}
-function getSegment (url: string, statusCodeExpected = 200) {
- return makeRawRequest(url, statusCodeExpected)
+function getSegment (url: string, statusCodeExpected = 200, range?: string) {
+ return makeRawRequest(url, statusCodeExpected, range)
}
function getSegmentSha256 (url: string, statusCodeExpected = 200) {
return makeRawRequest(url, statusCodeExpected)
}
+async function checkSegmentHash (
+ baseUrlPlaylist: string,
+ baseUrlSegment: string,
+ videoUUID: string,
+ resolution: number,
+ hlsPlaylist: VideoStreamingPlaylist
+) {
+ const res = await getPlaylist(`${baseUrlPlaylist}/${videoUUID}/${resolution}.m3u8`)
+ const playlist = res.text
+
+ const videoName = `${videoUUID}-${resolution}-fragmented.mp4`
+
+ const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist)
+
+ const length = parseInt(matches[1], 10)
+ const offset = parseInt(matches[2], 10)
+ const range = `${offset}-${offset + length - 1}`
+
+ const res2 = await getSegment(`${baseUrlSegment}/${videoUUID}/${videoName}`, 206, `bytes=${range}`)
+
+ const resSha = await getSegmentSha256(hlsPlaylist.segmentsSha256Url)
+
+ const sha256Server = resSha.body[ videoName ][range]
+ expect(sha256(res2.body)).to.equal(sha256Server)
+}
+
// ---------------------------------------------------------------------------
export {
getPlaylist,
getSegment,
- getSegmentSha256
+ getSegmentSha256,
+ checkSegmentHash
}
# yarn lockfile v1
-"@babel/polyfill@^7.2.5":
- version "7.2.5"
- resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.2.5.tgz#6c54b964f71ad27edddc567d065e57e87ed7fa7d"
- integrity sha512-8Y/t3MWThtMLYr0YNC/Q76tqN1w30+b0uQMeFUYauG2UGTR19zyUtFrAzT23zNtBxPp+LbE5E/nwV/q/r3y6ug==
- dependencies:
- core-js "^2.5.7"
- regenerator-runtime "^0.12.0"
-
"@iamstarkov/listr-update-renderer@0.4.1":
version "0.4.1"
resolved "https://registry.yarnpkg.com/@iamstarkov/listr-update-renderer/-/listr-update-renderer-0.4.1.tgz#d7c48092a2dcf90fd672b6c8b458649cb350c77e"
resolved "https://registry.yarnpkg.com/hide-powered-by/-/hide-powered-by-1.0.0.tgz#4a85ad65881f62857fc70af7174a1184dccce32b"
integrity sha1-SoWtZYgfYoV/xwr3F0oRhNzM4ys=
-"hlsdownloader@https://github.com/Chocobozzz/hlsdownloader#build":
- version "0.0.0-semantic-release"
- resolved "https://github.com/Chocobozzz/hlsdownloader#e19f9d803dcfe7ec25fd734b4743184f19a9b0cc"
- dependencies:
- "@babel/polyfill" "^7.2.5"
- async "^2.6.1"
- minimist "^1.2.0"
- mkdirp "^0.5.1"
- request "^2.88.0"
- request-promise "^4.2.2"
-
hosted-git-info@^2.1.4, hosted-git-info@^2.6.0, hosted-git-info@^2.7.1:
version "2.7.1"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=
-lodash@^4.0.0, lodash@^4.13.1, lodash@^4.17.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.5, lodash@^4.3.0, lodash@^4.8.2, lodash@~4.17.10:
+lodash@^4.0.0, lodash@^4.17.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.5, lodash@^4.3.0, lodash@^4.8.2, lodash@~4.17.10:
version "4.17.11"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.29.tgz#60f580d360170bb722a797cc704411e6da850c67"
integrity sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==
-psl@^1.1.28:
- version "1.1.31"
- resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184"
- integrity sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==
-
pstree.remy@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.2.tgz#4448bbeb4b2af1fed242afc8dc7416a6f504951a"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
-punycode@^2.1.0, punycode@^2.1.1:
+punycode@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.12.tgz#311bf0c6b63cd782f228a81abe146a2bfa9c56f2"
integrity sha512-n+IyV+nGz3+0q3/Yf1ra12KpCyi001bi4XFxSjbiWWjfqb52iTTtpGXmCCAOWWIAn9KEuFZKGqBERHmrtScZ3A==
-regenerator-runtime@^0.12.0:
- version "0.12.1"
- resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
- integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==
-
regex-not@^1.0.0, regex-not@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
-request-promise-core@1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.1.tgz#3eee00b2c5aa83239cfb04c5700da36f81cd08b6"
- integrity sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=
- dependencies:
- lodash "^4.13.1"
-
-request-promise@^4.2.2:
- version "4.2.2"
- resolved "https://registry.yarnpkg.com/request-promise/-/request-promise-4.2.2.tgz#d1ea46d654a6ee4f8ee6a4fea1018c22911904b4"
- integrity sha1-0epG1lSm7k+O5qT+oQGMIpEZBLQ=
- dependencies:
- bluebird "^3.5.0"
- request-promise-core "1.1.1"
- stealthy-require "^1.1.0"
- tough-cookie ">=2.3.3"
-
request@^2.74.0, request@^2.81.0, request@^2.83.0, request@^2.87.0, request@^2.88.0:
version "2.88.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==
-stealthy-require@^1.1.0:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
- integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
-
stream-each@^1.1.0:
version "1.2.3"
resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae"
dependencies:
nopt "~1.0.10"
-tough-cookie@>=2.3.3:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2"
- integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==
- dependencies:
- ip-regex "^2.1.0"
- psl "^1.1.28"
- punycode "^2.1.1"
-
tough-cookie@~2.4.3:
version "2.4.3"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"