Skip to content

Commit 06f5611

Browse files
Roki100alexanderpaolinifentTimeForANinjaVoltrexKeyva
authored
feat: add IPv6 block rotating (#713)
* IPv6 Rotating ^-^ * linter got mad at missing semicolon nice * Fixing format Fixing the format of files so eslint does not throw any errors. * adding colon * added test for IPv6 Block * Forgot to lint * Info test and download test improvement * use net instead if .includes * remove chunking-related stuff * Update lib/util.js Co-authored-by: fent <fentbox@gmail.com> * improve tests, fix the check in util * add test for invalid subnet * place done's on separate lines * fix typo in readme * fixing README.md * fixed mistake * fixed other mistake * Update README.md Co-authored-by: fent <fentbox@gmail.com> * Update README.md Co-authored-by: fent <fentbox@gmail.com> * fix conflicts * i forgot about this change * Fix undefined testInfo * ignoring invalid this * fix info test * fix million's await * remove useless part * Adding requested test * remove unrelated dependency * remove unused option * update readme * lint ipv6 example * remove ip6 dependencie * improve test coverage * (es)lint * Update lib/utils.js Co-authored-by: Voltrex <mohammadkeyvanzade94@gmail.com> Co-authored-by: MILLION <apaolini900o@outlook.com> Co-authored-by: Million900o <apaolini900o@gmail.com> Co-authored-by: fent <fentbox@gmail.com> Co-authored-by: MILLION <30964205+Million900o@users.noreply.github.com> Co-authored-by: TimeForANinja <t.kutscha@yahoo.de> Co-authored-by: TimeForANinja <TimeForANinja@users.noreply.github.com> Co-authored-by: Voltrex <mohammadkeyvanzade94@gmail.com>
1 parent b35bdad commit 06f5611

File tree

10 files changed

+290
-1
lines changed

10 files changed

+290
-1
lines changed

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Attempts to download a video from the given url. Returns a [readable stream](htt
3636
* `liveBuffer` - How much time buffer to use for live videos in milliseconds. Default is `20000`.
3737
* `highWaterMark` - How much of the video download to buffer into memory. See [node's docs](https://nodejs.org/api/stream.html#stream_constructor_new_stream_writable_options) for more. Defaults to 512KB.
3838
* `dlChunkSize` - When the chosen format is video only or audio only, the download is separated into multiple chunks to avoid throttling. This option specifies the size of each chunk in bytes. Setting it to 0 disables chunking. Defaults to 10MB.
39+
* `IPv6Block` - IPv6 block to rotate through, an alternative to using a proxy. [Read more](#How-does-using-an-IPv6-block-help?). Defaults to `undefined`.
3940

4041
#### Event: info
4142
* [`ytdl.videoInfo`](typings/index.d.ts#L194) - Info.
@@ -156,6 +157,24 @@ ytdl cannot download videos that fall into the following
156157

157158
Generated download links are valid for 6 hours, and may only be downloadable from the same IP address.
158159

160+
### Ratelimits
161+
When doing to many requests YouTube might block. This will result in your requests getting denied with HTTP-StatusCode 429. The following Steps might help you:
162+
* Update ytdl-core to the latest version
163+
* Use proxies (you can find an example [here](https://github.com/fent/node-ytdl-core/blob/master/example/proxy.js))
164+
* Extend on the Proxy Idea by rotating (IPv6-)Addresses
165+
* read [this](#How-does-using-an-IPv6-block-help?) for more information about this
166+
* Use cookies (you can find an example [here](https://github.com/fent/node-ytdl-core/blob/master/example/cookies.js))
167+
* for this to take effect you have to FIRST wait for the current ratelimit to expire
168+
* Wait it out (it usually goes away within a few days)
169+
170+
#### How does using an IPv6 block help?
171+
172+
For request-intensive tasks it might be useful to spread your requests across multiple source IP-Addresses. Changing the source IP that you use is similar to using a proxy, except without bypassing restrictions such as a region lock. More IP-Addresses result in less requests per IP and therefor increase your ratelimit. Since IPv4 Addresses are a limited Resource we advise to use IPv6.
173+
174+
Using an IPv6 block is essentially having millions of IPv6 addresses at your request. In a /64 IPv6 block (which is usually the Block given to a single Household), there are 18,446,744,073,709,551,616 unique IPv6 addresses. This would allow you to make each request with a different IPv6 address.
175+
176+
Even though using an IP-Block does help against ratelimits it requires you to setup your host system to accept http traffic from every message in an IP-Block. We can not help you with the setup for any specific host / hosting provider but searching the internet most likely can.
177+
159178
## Handling Separate Streams
160179

161180
Typically 1080p or better videos do not have audio encoded with it. The audio must be downloaded separately and merged via an encoding library. `ffmpeg` is the most widely used tool, with many [Node.js modules available](https://www.npmjs.com/search?q=ffmpeg). Use the `format` objects returned from `ytdl.getInfo` to download specific streams to combine to fit your needs. Look at [example/ffmpeg.js](example/ffmpeg.js) for an example on doing this.

example/ipv6_rotating.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const ytdl = require('..');
4+
5+
const options = {
6+
quality: 'highest',
7+
IPv6Block: '2001:2::/48',
8+
// Example /48 block provided by:
9+
// https://www.iana.org/assignments/ipv6-unicast-address-assignments/ipv6-unicast-address-assignments.xhtml
10+
};
11+
const url = 'https://www.youtube.com/watch?v=WhXefyLs-uw';
12+
const output = path.resolve(__dirname, 'video.mp4');
13+
14+
const video = ytdl(url, options);
15+
video.pipe(fs.createWriteStream(output));
16+
console.log('Downloading...');
17+
video.on('end', () => {
18+
console.log('Finished downloading.');
19+
});

lib/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,13 @@ const downloadFromInfoCallback = (stream, info, options) => {
9797
stream.emit('progress', chunk.length, downloaded, contentLength);
9898
};
9999

100+
if (options.IPv6Block) {
101+
options.requestOptions = Object.assign({}, options.requestOptions, {
102+
family: 6,
103+
localAddress: utils.getRandomIPv6(options.IPv6Block),
104+
});
105+
}
106+
100107
// Download the file in chunks, in this case the default is 10MB,
101108
// anything over this will cause youtube to throttle the download
102109
const dlChunkSize = options.dlChunkSize || 1024 * 1024 * 10;

lib/info.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ const AGE_RESTRICTED_URLS = [
4343
* @returns {Promise<Object>}
4444
*/
4545
exports.getBasicInfo = async(id, options) => {
46+
if (options.IPv6Block) {
47+
options.requestOptions = Object.assign({}, options.requestOptions, {
48+
family: 6,
49+
localAddress: utils.getRandomIPv6(options.IPv6Block),
50+
});
51+
}
4652
const retryOptions = Object.assign({}, miniget.defaultOptions, options.requestOptions);
4753
options.requestOptions = Object.assign({}, options.requestOptions, {});
4854
options.requestOptions.headers = Object.assign({},

lib/utils.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,76 @@ exports.checkForUpdates = () => {
181181
}
182182
return null;
183183
};
184+
185+
186+
/**
187+
* Gets random IPv6 Address from a block
188+
*
189+
* @param {string} ip the IPv6 block in CIDR-Notation
190+
* @returns {string}
191+
*/
192+
exports.getRandomIPv6 = ip => {
193+
// Start with a fast Regex-Check
194+
if (!isIPv6(ip)) throw Error('Invalid IPv6 format');
195+
// Start by splitting and normalizing addr and mask
196+
const [rawAddr, rawMask] = ip.split('/');
197+
let base10Mask = parseInt(rawMask);
198+
if (!base10Mask || base10Mask > 128 || base10Mask < 24) throw Error('Invalid IPv6 subnet');
199+
const base10addr = normalizeIP(rawAddr);
200+
// Get random addr to pad with
201+
// using Math.random since we're not requiring high level of randomness
202+
const randomAddr = new Array(8).fill(1).map(() => Math.floor(Math.random() * 0xffff));
203+
204+
// Merge base10addr with randomAddr
205+
const mergedAddr = randomAddr.map((randomItem, idx) => {
206+
// Calculate the amount of static bits
207+
const staticBits = Math.min(base10Mask, 16);
208+
// Adjust the bitmask with the staticBits
209+
base10Mask -= staticBits;
210+
// Calculate the bitmask
211+
// lsb makes the calculation way more complicated
212+
const mask = 0xffff - ((2 ** (16 - staticBits)) - 1);
213+
// Combine base10addr and random
214+
return (base10addr[idx] & mask) + (randomItem & (mask ^ 0xffff));
215+
});
216+
// Return new addr
217+
return mergedAddr.map(x => x.toString('16')).join(':');
218+
};
219+
220+
221+
// eslint-disable-next-line max-len
222+
const IPV6_REGEX = /^(([0-9a-f]{1,4}:)(:[0-9a-f]{1,4}){1,6}|([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}){1,5}|([0-9a-f]{1,4}:){1,3}(:[0-9a-f]{1,4}){1,4}|([0-9a-f]{1,4}:){1,4}(:[0-9a-f]{1,4}){1,3}|([0-9a-f]{1,4}:){1,5}(:[0-9a-f]{1,4}){1,2}|([0-9a-f]{1,4}:){1,6}(:[0-9a-f]{1,4})|([0-9a-f]{1,4}:){1,7}(([0-9a-f]{1,4})|:))\/(1[0-1]\d|12[0-8]|\d{1,2})$/;
223+
/**
224+
* Quick check for a valid IPv6
225+
* The Regex only accepts a subset of all IPv6 Addresses
226+
*
227+
* @param {string} ip the IPv6 block in CIDR-Notation to test
228+
* @returns {boolean} true if valid
229+
*/
230+
const isIPv6 = exports.isIPv6 = ip => IPV6_REGEX.test(ip);
231+
232+
233+
/**
234+
* Normalise an IP Address
235+
*
236+
* @param {string} ip the IPv6 Addr
237+
* @returns {number[]} the 8 parts of the IPv6 as Integers
238+
*/
239+
const normalizeIP = exports.normalizeIP = ip => {
240+
// Split by fill position
241+
const parts = ip.split('::').map(x => x.split(':'));
242+
// Normalize start and end
243+
const partStart = parts[0] || [];
244+
const partEnd = parts[1] || [];
245+
partEnd.reverse();
246+
// Placeholder for full ip
247+
const fullIP = new Array(8).fill(0);
248+
// Fill in start and end parts
249+
for (let i = 0; i < Math.min(partStart.length, 8); i++) {
250+
fullIP[i] = parseInt(partStart[i], 16) || 0;
251+
}
252+
for (let i = 0; i < Math.min(partEnd.length, 8); i++) {
253+
fullIP[7 - i] = parseInt(partEnd[i], 16) || 0;
254+
}
255+
return fullIP;
256+
};

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
"Andrew Kelley (https://github.com/andrewrk)",
1818
"Mauricio Allende (https://github.com/mallendeo)",
1919
"Rodrigo Altamirano (https://github.com/raltamirano)",
20-
"Jim Buck (https://github.com/JimmyBoh)"
20+
"Jim Buck (https://github.com/JimmyBoh)",
21+
"Paweł Ruciński (https://github.com/Roki100)",
22+
"Alexander Paolini (https://github.com/Million900o)"
2123
],
2224
"main": "./lib/index.js",
2325
"types": "./typings/index.d.ts",

test/download-test.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const streamEqual = require('stream-equal');
55
const sinon = require('sinon');
66
const nock = require('./nock');
77
const ytdl = require('..');
8+
const net = require('net');
89

910

1011
describe('Download video', () => {
@@ -545,6 +546,34 @@ describe('Download video', () => {
545546
});
546547
});
547548

549+
describe('With IPv6 Block', () => {
550+
it('Sends request with IPv6 address', done => {
551+
const stream = ytdl.downloadFromInfo(expectedInfo, { IPv6Block: '2001:2::/48' });
552+
stream.on('info', (info, format) => {
553+
nock.url(format.url).reply(function checkAddr() {
554+
// "this" is assigned by the function checkAddr
555+
// eslint-disable-next-line no-invalid-this
556+
assert.ok(net.isIPv6(this.req.options.localAddress));
557+
done();
558+
});
559+
});
560+
});
561+
});
562+
563+
describe('Without IPv6 Block', () => {
564+
it('Sends request with (default) IPv4 address', done => {
565+
const stream = ytdl.downloadFromInfo(expectedInfo);
566+
stream.on('info', (info, format) => {
567+
nock.url(format.url).reply(function checkAddr() {
568+
// "this" is assigned by the function checkAddr
569+
// eslint-disable-next-line no-invalid-this
570+
assert.ok(this.req.options.localAddress === undefined);
571+
done();
572+
});
573+
});
574+
});
575+
});
576+
548577
describe('with a bad filter', () => {
549578
it('Emits error', done => {
550579
const stream = ytdl.downloadFromInfo(expectedInfo, { filter: () => false });

test/full-info-test.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const ytdl = require('..');
22
const assert = require('assert-diff');
33
const nock = require('./nock');
44
const miniget = require('miniget');
5+
const net = require('net');
56

67

78
describe('ytdl.getInfo()', () => {
@@ -39,6 +40,27 @@ describe('ytdl.getInfo()', () => {
3940
});
4041
});
4142

43+
describe('With IPv6 Block', () => {
44+
it('Sends request with IPv6 address', async() => {
45+
const id = '_HSylqgVYQI';
46+
const scope = nock(id, 'regular');
47+
let info = await ytdl.getInfo(id, { IPv6Block: '2001:2::/48' });
48+
nock.url(info.formats[0].url).reply(function checkAddr() {
49+
// "this" is assigned by the function checkAddr
50+
// eslint-disable-next-line no-invalid-this
51+
assert.ok(net.isIPv6(this.req.options.localAddress));
52+
scope.done();
53+
});
54+
});
55+
});
56+
57+
describe('With invalid IPv6 Block', () => {
58+
it('Should give an error', async() => {
59+
const id = '_HSylqgVYQI';
60+
await assert.rejects(ytdl.getInfo(id, { IPv6Block: '2001:2::/200' }), /Invalid IPv6 format/);
61+
});
62+
});
63+
4264
describe('From a video with a cipher', () => {
4365
it('Retrieves deciphered video formats', async() => {
4466
const id = 'B3eAMGXFw1o';

test/utils-test.js

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,117 @@ describe('utils.checkForUpdates', () => {
181181
});
182182
});
183183

184+
describe('utils.isIPv6', () => {
185+
it('returns true for valid IPv6 net', () => {
186+
assert.ok(utils.isIPv6('100::/128'));
187+
assert.ok(utils.isIPv6('100::/119'));
188+
assert.ok(utils.isIPv6('100::/13'));
189+
assert.ok(utils.isIPv6('100::/1'));
190+
assert.ok(utils.isIPv6('20a::/13'));
191+
assert.ok(utils.isIPv6('0064:ff9b:0000:0000:0000:0000:1234:5678/13'));
192+
assert.ok(utils.isIPv6('0064:ff9b:0001:1122:0033:4400:0000:0001/13'));
193+
assert.ok(utils.isIPv6('fe80:4:6c:8c74:0000:5efe:afef:a89/13'));
194+
assert.ok(utils.isIPv6('fe80:4:6c:8c74:0000:5efe::a89/13'));
195+
assert.ok(utils.isIPv6('fe80:4:6c:8c74:0000::a89/13'));
196+
assert.ok(utils.isIPv6('fe80:4:6c:8c74::a89/13'));
197+
assert.ok(utils.isIPv6('fe80:4:6c::a89/13'));
198+
assert.ok(utils.isIPv6('fe80:4::a89/13'));
199+
assert.ok(utils.isIPv6('fe80::a89/13'));
200+
assert.ok(utils.isIPv6('fe80::/13'));
201+
assert.ok(utils.isIPv6('fea3:c65:43ee:54:e2a:2357:4ac4:732/13'));
202+
assert.ok(utils.isIPv6('fe80:1234:abc/13'));
203+
assert.ok(utils.isIPv6('20a:1234::1/13'));
204+
});
205+
206+
it('returns false for valid but unwanted IPv6 net', () => {
207+
assert.ok(!utils.isIPv6('::/1'));
208+
assert.ok(!utils.isIPv6('::1/1'));
209+
assert.ok(!utils.isIPv6('::ffff:10.0.0.3/1'));
210+
assert.ok(!utils.isIPv6('::10.0.0.3/1'));
211+
assert.ok(!utils.isIPv6('127.0.0.1/1'));
212+
assert.ok(!utils.isIPv6('24a6:57:c:36cf:0000:5efe:109.205.140.116/64'));
213+
});
214+
215+
it('returns false for invalid IPv6 net', () => {
216+
assert.ok(!utils.isIPv6('100::/129'));
217+
assert.ok(!utils.isIPv6('100::/130'));
218+
assert.ok(!utils.isIPv6('100::/abc'));
219+
assert.ok(!utils.isIPv6('100::'));
220+
assert.ok(!utils.isIPv6('fe80:4::8c74::5efe:afef:a89/64'));
221+
assert.ok(!utils.isIPv6('24a6:57:c:36cf:0000:5efe:ab:cd:ef/64'));
222+
assert.ok(!utils.isIPv6('24a6:57:c:36cf:0000:5efe::ab:cd/64'));
223+
});
224+
});
225+
226+
describe('utils.getRandomIPv6', () => {
227+
it('errors for completely invalid ipv6', () => {
228+
assert.throws(() => {
229+
utils.getRandomIPv6('some random string');
230+
}, /Invalid IPv6 format/);
231+
});
232+
233+
it('errors for invalid subnet sizes', () => {
234+
assert.throws(() => {
235+
utils.getRandomIPv6('fe80::/300');
236+
}, /Invalid IPv6 format/);
237+
assert.throws(() => {
238+
utils.getRandomIPv6('127::1/1');
239+
}, /Invalid IPv6 subnet/);
240+
assert.throws(() => {
241+
utils.getRandomIPv6('fe80::');
242+
}, /Invalid IPv6 format/);
243+
assert.throws(() => {
244+
utils.getRandomIPv6('fe80::/ff');
245+
}, /Invalid IPv6 format/);
246+
});
247+
248+
it('keeps the upper bits of the subnet', () => {
249+
for (let i = 24; i < 128; i++) {
250+
const ip = utils.getRandomIPv6(`ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/${i}`);
251+
const bits = ip.split(':').map(x => parseInt(x, 16).toString(2)).join('');
252+
assert.equal(bits.substr(0, i), '1'.repeat(i));
253+
}
254+
});
255+
256+
it('rolls random bits for the lower bits', () => {
257+
// Only testing to 64 and not 128
258+
// The second part of the random IP is tested to not be only onces
259+
// and rolling 8 full 0xff bytes should be unlikely enough
260+
for (let i = 24; i < 64; i++) {
261+
const ip = utils.getRandomIPv6(`ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/${i}`);
262+
const bits = ip.split(':').map(x => parseInt(x, 16).toString(2)).join('');
263+
assert.ok(bits.substr(i).split('').some(x => x === '0'));
264+
}
265+
});
266+
});
267+
268+
describe('utils.normalizeIP', () => {
269+
it('does work for already expanded ips', () => {
270+
assert.deepEqual(utils.normalizeIP('1:2:3:4:5:6:7:8'), [1, 2, 3, 4, 5, 6, 7, 8]);
271+
});
272+
273+
it('resolves bytes to integers', () => {
274+
assert.deepEqual(utils.normalizeIP('ffff'), [65535, 0, 0, 0, 0, 0, 0, 0]);
275+
});
276+
277+
it('expands ::', () => {
278+
assert.deepEqual(utils.normalizeIP('ab::cd'), [171, 0, 0, 0, 0, 0, 0, 205]);
279+
assert.deepEqual(utils.normalizeIP('ab:cd::ef'), [171, 205, 0, 0, 0, 0, 0, 239]);
280+
assert.deepEqual(utils.normalizeIP('ab:cd::12:ef'), [171, 205, 0, 0, 0, 0, 18, 239]);
281+
assert.deepEqual(utils.normalizeIP('ab:cd::'), [171, 205, 0, 0, 0, 0, 0, 0]);
282+
assert.deepEqual(utils.normalizeIP('123::'), [291, 0, 0, 0, 0, 0, 0, 0]);
283+
assert.deepEqual(utils.normalizeIP('0::'), [0, 0, 0, 0, 0, 0, 0, 0]);
284+
assert.deepEqual(utils.normalizeIP('::'), [0, 0, 0, 0, 0, 0, 0, 0]);
285+
assert.deepEqual(utils.normalizeIP('::ab:cd'), [0, 0, 0, 0, 0, 0, 171, 205]);
286+
});
287+
288+
it('does handle invalid ips', () => {
289+
assert.deepEqual(utils.normalizeIP('1:2:3:4:5::6:7:8::'), [1, 2, 3, 4, 5, 6, 7, 8]);
290+
assert.deepEqual(utils.normalizeIP('::1:2:3:4:5:6:7:8'), [1, 2, 3, 4, 5, 6, 7, 8]);
291+
assert.deepEqual(utils.normalizeIP('1:2:3:4:5::6:7:8:9:10'), [1, 2, 3, 6, 7, 8, 9, 16]);
292+
});
293+
});
294+
184295
describe('utils.exposedMiniget', () => {
185296
it('does not error with undefined requestOptionsOverwrite', async() => {
186297
const scope = nock('https://test.com').get('/').reply(200, 'nice');

typings/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ declare module 'ytdl-core' {
2525
begin?: string | number | Date;
2626
liveBuffer?: number;
2727
highWaterMark?: number;
28+
IPv6Block?: string;
2829
dlChunkSize?: number;
2930
}
3031

0 commit comments

Comments
 (0)