diff options
author | smilekison <24309222+smilekison@users.noreply.github.com> | 2021-08-25 06:08:37 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-08-25 15:08:37 +0200 |
commit | 8729a87024fc837f7dd6f13afeec90cf6dda1c06 (patch) | |
tree | 0f6f34966cf7b08d3024a08def4aa547d3aec459 | |
parent | fdec51e3846d50e3375612a6820ed3ab0b5fcd25 (diff) | |
download | PeerTube-8729a87024fc837f7dd6f13afeec90cf6dda1c06.tar.gz PeerTube-8729a87024fc837f7dd6f13afeec90cf6dda1c06.tar.zst PeerTube-8729a87024fc837f7dd6f13afeec90cf6dda1c06.zip |
Support proxies for PeerTube (#4346)
* Updated with latest code
* Updated Support proxies for PeerTube
* Support Proxies for PeerTube (Updated with change request)
* Cleanup proxy PR
Co-authored-by: Chocobozzz <me@florianbigard.com>
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | server/helpers/proxy.ts | 14 | ||||
-rw-r--r-- | server/helpers/requests.ts | 32 | ||||
-rw-r--r-- | server/tests/api/server/index.ts | 1 | ||||
-rw-r--r-- | server/tests/api/server/proxy.ts | 72 | ||||
-rw-r--r-- | shared/extra-utils/mock-servers/mock-proxy.ts | 27 | ||||
-rw-r--r-- | yarn.lock | 46 |
7 files changed, 191 insertions, 3 deletions
diff --git a/package.json b/package.json index 18bce1123..dc15405ce 100644 --- a/package.json +++ b/package.json | |||
@@ -99,6 +99,7 @@ | |||
99 | "fs-extra": "^10.0.0", | 99 | "fs-extra": "^10.0.0", |
100 | "got": "^11.8.2", | 100 | "got": "^11.8.2", |
101 | "helmet": "^4.1.0", | 101 | "helmet": "^4.1.0", |
102 | "hpagent": "^0.1.2", | ||
102 | "http-problem-details": "^0.1.5", | 103 | "http-problem-details": "^0.1.5", |
103 | "http-signature": "1.3.5", | 104 | "http-signature": "1.3.5", |
104 | "ip-anonymize": "^0.1.0", | 105 | "ip-anonymize": "^0.1.0", |
@@ -199,6 +200,7 @@ | |||
199 | "marked-man": "^0.7.0", | 200 | "marked-man": "^0.7.0", |
200 | "mocha": "^9.0.0", | 201 | "mocha": "^9.0.0", |
201 | "nodemon": "^2.0.1", | 202 | "nodemon": "^2.0.1", |
203 | "proxy": "^1.0.2", | ||
202 | "socket.io-client": "^4.0.1", | 204 | "socket.io-client": "^4.0.1", |
203 | "source-map-support": "^0.5.0", | 205 | "source-map-support": "^0.5.0", |
204 | "supertest": "^6.0.1", | 206 | "supertest": "^6.0.1", |
diff --git a/server/helpers/proxy.ts b/server/helpers/proxy.ts new file mode 100644 index 000000000..8b82ccae0 --- /dev/null +++ b/server/helpers/proxy.ts | |||
@@ -0,0 +1,14 @@ | |||
1 | function getProxy () { | ||
2 | return process.env.HTTPS_PROXY || | ||
3 | process.env.HTTP_PROXY || | ||
4 | undefined | ||
5 | } | ||
6 | |||
7 | function isProxyEnabled () { | ||
8 | return !!getProxy() | ||
9 | } | ||
10 | |||
11 | export { | ||
12 | getProxy, | ||
13 | isProxyEnabled | ||
14 | } | ||
diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts index 36e69458e..e09e23086 100644 --- a/server/helpers/requests.ts +++ b/server/helpers/requests.ts | |||
@@ -1,19 +1,21 @@ | |||
1 | import { createWriteStream, remove } from 'fs-extra' | 1 | import { createWriteStream, remove } from 'fs-extra' |
2 | import got, { CancelableRequest, Options as GotOptions, RequestError } from 'got' | 2 | import got, { CancelableRequest, Options as GotOptions, RequestError } from 'got' |
3 | import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent' | ||
3 | import { join } from 'path' | 4 | import { join } from 'path' |
4 | import { CONFIG } from '../initializers/config' | 5 | import { CONFIG } from '../initializers/config' |
5 | import { ACTIVITY_PUB, PEERTUBE_VERSION, REQUEST_TIMEOUT, WEBSERVER } from '../initializers/constants' | 6 | import { ACTIVITY_PUB, PEERTUBE_VERSION, REQUEST_TIMEOUT, WEBSERVER } from '../initializers/constants' |
6 | import { pipelinePromise } from './core-utils' | 7 | import { pipelinePromise } from './core-utils' |
7 | import { processImage } from './image-utils' | 8 | import { processImage } from './image-utils' |
8 | import { logger } from './logger' | 9 | import { logger } from './logger' |
10 | import { getProxy, isProxyEnabled } from './proxy' | ||
11 | |||
12 | const httpSignature = require('http-signature') | ||
9 | 13 | ||
10 | export interface PeerTubeRequestError extends Error { | 14 | export interface PeerTubeRequestError extends Error { |
11 | statusCode?: number | 15 | statusCode?: number |
12 | responseBody?: any | 16 | responseBody?: any |
13 | } | 17 | } |
14 | 18 | ||
15 | const httpSignature = require('http-signature') | ||
16 | |||
17 | type PeerTubeRequestOptions = { | 19 | type PeerTubeRequestOptions = { |
18 | activityPub?: boolean | 20 | activityPub?: boolean |
19 | bodyKBLimit?: number // 1MB | 21 | bodyKBLimit?: number // 1MB |
@@ -29,6 +31,8 @@ type PeerTubeRequestOptions = { | |||
29 | } & Pick<GotOptions, 'headers' | 'json' | 'method' | 'searchParams'> | 31 | } & Pick<GotOptions, 'headers' | 'json' | 'method' | 'searchParams'> |
30 | 32 | ||
31 | const peertubeGot = got.extend({ | 33 | const peertubeGot = got.extend({ |
34 | ...getAgent(), | ||
35 | |||
32 | headers: { | 36 | headers: { |
33 | 'user-agent': getUserAgent() | 37 | 'user-agent': getUserAgent() |
34 | }, | 38 | }, |
@@ -153,6 +157,30 @@ async function downloadImage (url: string, destDir: string, destName: string, si | |||
153 | } | 157 | } |
154 | } | 158 | } |
155 | 159 | ||
160 | function getAgent () { | ||
161 | if (!isProxyEnabled()) return {} | ||
162 | |||
163 | const proxy = getProxy() | ||
164 | |||
165 | logger.info('Using proxy %s.', proxy) | ||
166 | |||
167 | const proxyAgentOptions = { | ||
168 | keepAlive: true, | ||
169 | keepAliveMsecs: 1000, | ||
170 | maxSockets: 256, | ||
171 | maxFreeSockets: 256, | ||
172 | scheduling: 'lifo' as 'lifo', | ||
173 | proxy | ||
174 | } | ||
175 | |||
176 | return { | ||
177 | agent: { | ||
178 | http: new HttpProxyAgent(proxyAgentOptions), | ||
179 | https: new HttpsProxyAgent(proxyAgentOptions) | ||
180 | } | ||
181 | } | ||
182 | } | ||
183 | |||
156 | function getUserAgent () { | 184 | function getUserAgent () { |
157 | return `PeerTube/${PEERTUBE_VERSION} (+${WEBSERVER.URL})` | 185 | return `PeerTube/${PEERTUBE_VERSION} (+${WEBSERVER.URL})` |
158 | } | 186 | } |
diff --git a/server/tests/api/server/index.ts b/server/tests/api/server/index.ts index 56e6eb5da..b16a22ee7 100644 --- a/server/tests/api/server/index.ts +++ b/server/tests/api/server/index.ts | |||
@@ -15,3 +15,4 @@ import './stats' | |||
15 | import './tracker' | 15 | import './tracker' |
16 | import './no-client' | 16 | import './no-client' |
17 | import './plugins' | 17 | import './plugins' |
18 | import './proxy' | ||
diff --git a/server/tests/api/server/proxy.ts b/server/tests/api/server/proxy.ts new file mode 100644 index 000000000..d5042ef27 --- /dev/null +++ b/server/tests/api/server/proxy.ts | |||
@@ -0,0 +1,72 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import * as chai from 'chai' | ||
5 | import { cleanupTests, createMultipleServers, doubleFollow, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@shared/extra-utils' | ||
6 | import { MockProxy } from '@shared/extra-utils/mock-servers/mock-proxy' | ||
7 | |||
8 | const expect = chai.expect | ||
9 | |||
10 | describe('Test proxy', function () { | ||
11 | let servers: PeerTubeServer[] = [] | ||
12 | let proxy: MockProxy | ||
13 | |||
14 | const goodEnv = { HTTP_PROXY: '' } | ||
15 | const badEnv = { HTTP_PROXY: 'http://localhost:9000' } | ||
16 | |||
17 | before(async function () { | ||
18 | this.timeout(120000) | ||
19 | |||
20 | proxy = new MockProxy() | ||
21 | |||
22 | const proxyPort = await proxy.initialize() | ||
23 | servers = await createMultipleServers(2) | ||
24 | |||
25 | goodEnv.HTTP_PROXY = 'http://localhost:' + proxyPort | ||
26 | |||
27 | await setAccessTokensToServers(servers) | ||
28 | await doubleFollow(servers[0], servers[1]) | ||
29 | }) | ||
30 | |||
31 | it('Should succeed federation with the appropriate proxy config', async function () { | ||
32 | await servers[0].kill() | ||
33 | await servers[0].run({}, { env: goodEnv }) | ||
34 | |||
35 | await servers[0].videos.quickUpload({ name: 'video 1' }) | ||
36 | |||
37 | await waitJobs(servers) | ||
38 | |||
39 | for (const server of servers) { | ||
40 | const { total, data } = await server.videos.list() | ||
41 | expect(total).to.equal(1) | ||
42 | expect(data).to.have.lengthOf(1) | ||
43 | } | ||
44 | }) | ||
45 | |||
46 | it('Should fail federation with a wrong proxy config', async function () { | ||
47 | await servers[0].kill() | ||
48 | await servers[0].run({}, { env: badEnv }) | ||
49 | |||
50 | await servers[0].videos.quickUpload({ name: 'video 2' }) | ||
51 | |||
52 | await waitJobs(servers) | ||
53 | |||
54 | { | ||
55 | const { total, data } = await servers[0].videos.list() | ||
56 | expect(total).to.equal(2) | ||
57 | expect(data).to.have.lengthOf(2) | ||
58 | } | ||
59 | |||
60 | { | ||
61 | const { total, data } = await servers[1].videos.list() | ||
62 | expect(total).to.equal(1) | ||
63 | expect(data).to.have.lengthOf(1) | ||
64 | } | ||
65 | }) | ||
66 | |||
67 | after(async function () { | ||
68 | proxy.terminate() | ||
69 | |||
70 | await cleanupTests(servers) | ||
71 | }) | ||
72 | }) | ||
diff --git a/shared/extra-utils/mock-servers/mock-proxy.ts b/shared/extra-utils/mock-servers/mock-proxy.ts new file mode 100644 index 000000000..5365f87d1 --- /dev/null +++ b/shared/extra-utils/mock-servers/mock-proxy.ts | |||
@@ -0,0 +1,27 @@ | |||
1 | |||
2 | import { createServer, Server } from 'http' | ||
3 | import * as proxy from 'proxy' | ||
4 | import { randomInt } from '@shared/core-utils' | ||
5 | |||
6 | class MockProxy { | ||
7 | private server: Server | ||
8 | |||
9 | initialize () { | ||
10 | return new Promise<number>(res => { | ||
11 | const port = 42501 + randomInt(1, 100) | ||
12 | |||
13 | this.server = proxy(createServer()) | ||
14 | this.server.listen(port, () => res(port)) | ||
15 | }) | ||
16 | } | ||
17 | |||
18 | terminate () { | ||
19 | if (this.server) this.server.close() | ||
20 | } | ||
21 | } | ||
22 | |||
23 | // --------------------------------------------------------------------------- | ||
24 | |||
25 | export { | ||
26 | MockProxy | ||
27 | } | ||
@@ -2002,6 +2002,16 @@ argparse@^2.0.1: | |||
2002 | resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" | 2002 | resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" |
2003 | integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== | 2003 | integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== |
2004 | 2004 | ||
2005 | args@5.0.1: | ||
2006 | version "5.0.1" | ||
2007 | resolved "https://registry.yarnpkg.com/args/-/args-5.0.1.tgz#4bf298df90a4799a09521362c579278cc2fdd761" | ||
2008 | integrity sha512-1kqmFCFsPffavQFGt8OxJdIcETti99kySRUPMpOhaGjL6mRJn8HFU1OxKY5bMqfZKUwTQc1mZkAjmGYaVOHFtQ== | ||
2009 | dependencies: | ||
2010 | camelcase "5.0.0" | ||
2011 | chalk "2.4.2" | ||
2012 | leven "2.1.0" | ||
2013 | mri "1.1.4" | ||
2014 | |||
2005 | array-differ@^3.0.0: | 2015 | array-differ@^3.0.0: |
2006 | version "3.0.0" | 2016 | version "3.0.0" |
2007 | resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" | 2017 | resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" |
@@ -2200,6 +2210,11 @@ basic-auth-connect@^1.0.0: | |||
2200 | resolved "https://registry.yarnpkg.com/basic-auth-connect/-/basic-auth-connect-1.0.0.tgz#fdb0b43962ca7b40456a7c2bb48fe173da2d2122" | 2210 | resolved "https://registry.yarnpkg.com/basic-auth-connect/-/basic-auth-connect-1.0.0.tgz#fdb0b43962ca7b40456a7c2bb48fe173da2d2122" |
2201 | integrity sha1-/bC0OWLKe0BFanwrtI/hc9otISI= | 2211 | integrity sha1-/bC0OWLKe0BFanwrtI/hc9otISI= |
2202 | 2212 | ||
2213 | basic-auth-parser@0.0.2: | ||
2214 | version "0.0.2" | ||
2215 | resolved "https://registry.yarnpkg.com/basic-auth-parser/-/basic-auth-parser-0.0.2.tgz#ce9e71a77f23c1279eecd2659b2a46244c156e41" | ||
2216 | integrity sha1-zp5xp38jwSee7NJlmypGJEwVbkE= | ||
2217 | |||
2203 | basic-auth@2.0.1, basic-auth@~2.0.1: | 2218 | basic-auth@2.0.1, basic-auth@~2.0.1: |
2204 | version "2.0.1" | 2219 | version "2.0.1" |
2205 | resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" | 2220 | resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" |
@@ -2616,6 +2631,11 @@ camelcase-keys@^4.0.0: | |||
2616 | map-obj "^2.0.0" | 2631 | map-obj "^2.0.0" |
2617 | quick-lru "^1.0.0" | 2632 | quick-lru "^1.0.0" |
2618 | 2633 | ||
2634 | camelcase@5.0.0: | ||
2635 | version "5.0.0" | ||
2636 | resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" | ||
2637 | integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== | ||
2638 | |||
2619 | camelcase@^4.1.0: | 2639 | camelcase@^4.1.0: |
2620 | version "4.1.0" | 2640 | version "4.1.0" |
2621 | resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" | 2641 | resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" |
@@ -2663,7 +2683,7 @@ chai@^4.1.1: | |||
2663 | pathval "^1.1.1" | 2683 | pathval "^1.1.1" |
2664 | type-detect "^4.0.5" | 2684 | type-detect "^4.0.5" |
2665 | 2685 | ||
2666 | chalk@^2.0.0, chalk@^2.4.2: | 2686 | chalk@2.4.2, chalk@^2.0.0, chalk@^2.4.2: |
2667 | version "2.4.2" | 2687 | version "2.4.2" |
2668 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" | 2688 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" |
2669 | integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== | 2689 | integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== |
@@ -4692,6 +4712,11 @@ hosted-git-info@^2.1.4: | |||
4692 | resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" | 4712 | resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" |
4693 | integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== | 4713 | integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== |
4694 | 4714 | ||
4715 | hpagent@^0.1.2: | ||
4716 | version "0.1.2" | ||
4717 | resolved "https://registry.yarnpkg.com/hpagent/-/hpagent-0.1.2.tgz#cab39c66d4df2d4377dbd212295d878deb9bdaa9" | ||
4718 | integrity sha512-ePqFXHtSQWAFXYmj+JtOTHr84iNrII4/QRlAAPPE+zqnKy4xJo7Ie1Y4kC7AdB+LxLxSTTzBMASsEcy0q8YyvQ== | ||
4719 | |||
4695 | html-to-text@8.0.0: | 4720 | html-to-text@8.0.0: |
4696 | version "8.0.0" | 4721 | version "8.0.0" |
4697 | resolved "https://registry.yarnpkg.com/html-to-text/-/html-to-text-8.0.0.tgz#5848681a5a38d657a7bb58cf5006d1c29fe64ce3" | 4722 | resolved "https://registry.yarnpkg.com/html-to-text/-/html-to-text-8.0.0.tgz#5848681a5a38d657a7bb58cf5006d1c29fe64ce3" |
@@ -5548,6 +5573,11 @@ latest-version@^5.0.0: | |||
5548 | dependencies: | 5573 | dependencies: |
5549 | package-json "^6.3.0" | 5574 | package-json "^6.3.0" |
5550 | 5575 | ||
5576 | leven@2.1.0: | ||
5577 | version "2.1.0" | ||
5578 | resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580" | ||
5579 | integrity sha1-wuep93IJTe6dNCAq6KzORoeHVYA= | ||
5580 | |||
5551 | levn@^0.4.1: | 5581 | levn@^0.4.1: |
5552 | version "0.4.1" | 5582 | version "0.4.1" |
5553 | resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" | 5583 | resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" |
@@ -6196,6 +6226,11 @@ mp4-stream@^3.0.0: | |||
6196 | queue-microtask "^1.2.2" | 6226 | queue-microtask "^1.2.2" |
6197 | readable-stream "^3.0.6" | 6227 | readable-stream "^3.0.6" |
6198 | 6228 | ||
6229 | mri@1.1.4: | ||
6230 | version "1.1.4" | ||
6231 | resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.4.tgz#7cb1dd1b9b40905f1fac053abe25b6720f44744a" | ||
6232 | integrity sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w== | ||
6233 | |||
6199 | ms@2.0.0: | 6234 | ms@2.0.0: |
6200 | version "2.0.0" | 6235 | version "2.0.0" |
6201 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" | 6236 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" |
@@ -7131,6 +7166,15 @@ proxy-addr@~2.0.5: | |||
7131 | forwarded "0.2.0" | 7166 | forwarded "0.2.0" |
7132 | ipaddr.js "1.9.1" | 7167 | ipaddr.js "1.9.1" |
7133 | 7168 | ||
7169 | proxy@^1.0.2: | ||
7170 | version "1.0.2" | ||
7171 | resolved "https://registry.yarnpkg.com/proxy/-/proxy-1.0.2.tgz#e0cfbe11c0a7a8b238fd2d7134de4e2867578e7f" | ||
7172 | integrity sha512-KNac2ueWRpjbUh77OAFPZuNdfEqNynm9DD4xHT14CccGpW8wKZwEkN0yjlb7X9G9Z9F55N0Q+1z+WfgAhwYdzQ== | ||
7173 | dependencies: | ||
7174 | args "5.0.1" | ||
7175 | basic-auth-parser "0.0.2" | ||
7176 | debug "^4.1.1" | ||
7177 | |||
7134 | pseudomap@^1.0.2: | 7178 | pseudomap@^1.0.2: |
7135 | version "1.0.2" | 7179 | version "1.0.2" |
7136 | resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" | 7180 | resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" |