@unavatar/core 3.46.0 → 3.46.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@unavatar/core",
3
3
  "description": "Get unified user avatar from social networks, including Instagram, SoundCloud, Telegram, Twitter, YouTube & more.",
4
4
  "homepage": "https://unavatar.io",
5
- "version": "3.46.0",
5
+ "version": "3.46.1",
6
6
  "main": "src/index.js",
7
7
  "exports": {
8
8
  ".": "./src/index.js",
@@ -8,6 +8,7 @@ const isEmail = require('is-email-like')
8
8
  const pTimeout = require('p-timeout')
9
9
  const pAny = require('p-any')
10
10
 
11
+ const { RESERVED_ADDRESS_CODE } = require('../util/is-reserved-ip')
11
12
  const httpStatus = require('../util/http-status')
12
13
  const isIterable = require('../util/is-iterable')
13
14
  const ExtendableError = require('../util/error')
@@ -23,9 +24,29 @@ const getInputType = input => {
23
24
  return 'username'
24
25
  }
25
26
 
26
- const factory = ({ constants, providers, providerTiers, reachableUrl }) => {
27
+ const factory = ({
28
+ constants,
29
+ providers,
30
+ providerTiers,
31
+ reachableUrl,
32
+ isReservedIp
33
+ }) => {
27
34
  const { REQUEST_TIMEOUT } = constants
28
35
 
36
+ const refuseReservedAddress = provider => {
37
+ throw new ExtendableError({
38
+ message: 'The URL points to a reserved address.',
39
+ provider,
40
+ statusCode: httpStatus.FORBIDDEN
41
+ })
42
+ }
43
+
44
+ const assertPublicUrl = async (url, provider) => {
45
+ if (await isReservedIp(new URL(url).hostname)) {
46
+ refuseReservedAddress(provider)
47
+ }
48
+ }
49
+
29
50
  const getAvatarContent = provider => async output => {
30
51
  if (typeof output !== 'string' || output === '') {
31
52
  const message =
@@ -49,7 +70,12 @@ const factory = ({ constants, providers, providerTiers, reachableUrl }) => {
49
70
  })
50
71
  }
51
72
 
52
- const { statusCode, url } = await reachableUrl(output)
73
+ await assertPublicUrl(output, provider)
74
+
75
+ const { statusCode, url, reservedAddress } = await reachableUrl(output)
76
+
77
+ if (reservedAddress) refuseReservedAddress(provider)
78
+ await assertPublicUrl(url, provider)
53
79
 
54
80
  if (!reachableUrl.isReachable({ statusCode })) {
55
81
  throw new ExtendableError({
@@ -73,6 +99,9 @@ const factory = ({ constants, providers, providerTiers, reachableUrl }) => {
73
99
  .then(getAvatarContent(provider))
74
100
  .catch(error => {
75
101
  isIterable.forEach(error, error => {
102
+ if (error.code === RESERVED_ADDRESS_CODE) {
103
+ error.statusCode = httpStatus.FORBIDDEN
104
+ }
76
105
  error.statusCode = error.statusCode ?? error.response?.statusCode
77
106
  error.provider = provider
78
107
  })
package/src/index.js CHANGED
@@ -29,23 +29,20 @@ module.exports = ({
29
29
  resolver: dnsResolver
30
30
  })
31
31
  const isReservedIp = require('./util/is-reserved-ip')({ cacheableLookup })
32
- const got = require('./util/got')({ cacheableLookup })
32
+ const got = require('./util/got')({ cacheableLookup, isReservedIp })
33
33
  const reachableUrl = require('./util/reachable-url')({
34
34
  got,
35
35
  pingCache: cache.pingCache
36
36
  })
37
37
  const createBrowser = require('./util/browserless')(constants)
38
38
  const getHTML = require('./util/html-get')({ createBrowser, got })
39
- const {
40
- createHtmlProvider,
41
- getOgImage,
42
- NOT_FOUND
43
- } = require('./util/html-provider')({
44
- ...constants,
45
- getHTML,
46
- onFetchHTML,
47
- userAgent
48
- })
39
+ const { createHtmlProvider, getOgImage, NOT_FOUND } =
40
+ require('./util/html-provider')({
41
+ ...constants,
42
+ getHTML,
43
+ onFetchHTML,
44
+ userAgent
45
+ })
49
46
 
50
47
  const providerCtx = {
51
48
  constants,
@@ -60,15 +57,15 @@ module.exports = ({
60
57
  githubSearchCache: cache.githubSearchCache,
61
58
  itunesSearchCache: cache.itunesSearchCache
62
59
  }
63
- const { providers, providerTiers, providersBy } = require('./providers')(
64
- providerCtx
65
- )
60
+ const { providers, providerTiers, providersBy } =
61
+ require('./providers')(providerCtx)
66
62
 
67
63
  const { auto, getInputType, getAvatar } = require('./avatar/auto')({
68
64
  constants,
69
65
  providers,
70
66
  providerTiers,
71
- reachableUrl
67
+ reachableUrl,
68
+ isReservedIp
72
69
  })
73
70
 
74
71
  const unavatar = input => auto(getInputType(input))(input, {})
@@ -4,18 +4,15 @@ const createGetLogo = require('bimi-url')
4
4
 
5
5
  const parseInput = input => input.slice(input.lastIndexOf('@') + 1)
6
6
 
7
- module.exports = ({ bimiCache, dnsResolver, got, isReservedIp }) => {
7
+ module.exports = ({ bimiCache, dnsResolver, got }) => {
8
8
  const getLogo = createGetLogo({
9
9
  gotOpts: got.gotOpts,
10
10
  keyvOpts: bimiCache,
11
11
  resolveTxt: hostname => dnsResolver.resolveTxt(hostname)
12
12
  })
13
13
 
14
- return async function bimi (input) {
15
- const logoUrl = await getLogo(parseInput(input))
16
- if (!logoUrl) return undefined
17
- if (await isReservedIp(new URL(logoUrl).hostname)) return undefined
18
- return logoUrl
14
+ return function bimi (input) {
15
+ return getLogo(parseInput(input))
19
16
  }
20
17
  }
21
18
 
package/src/util/got.js CHANGED
@@ -5,6 +5,8 @@ const tlsHook = require('https-tls/hook')
5
5
  const uaHints = require('ua-hints')
6
6
  const got = require('got')
7
7
 
8
+ const { RESERVED_ADDRESS_CODE } = require('./is-reserved-ip')
9
+
8
10
  const topUserAgents = require('top-user-agents')
9
11
  const randomUserAgent = uniqueRandomArray(topUserAgents)
10
12
 
@@ -36,11 +38,40 @@ const userAgentHook = options => {
36
38
  }
37
39
  }
38
40
 
39
- module.exports = ({ cacheableLookup }) => {
41
+ module.exports = ({ cacheableLookup, isReservedIp }) => {
42
+ /**
43
+ * The refused address is reported on `options.context` because a caller
44
+ * reading the outcome through `reachable-url` never sees the rejection: it is
45
+ * swallowed there and served back as a response.
46
+ *
47
+ * Only a context the caller owns is written: the one got defaults to is
48
+ * frozen and shared between requests, so asking to be told is what passing a
49
+ * context means.
50
+ *
51
+ * got replaces the error with a RequestError that keeps the message and the
52
+ * code, so the refusal is named by a code rather than by its type.
53
+ */
54
+ const reservedAddressHook = async options => {
55
+ const hostname = options.url?.hostname
56
+ if (hostname && (await isReservedIp(hostname))) {
57
+ if (Object.isExtensible(options.context)) {
58
+ options.context.reservedAddress = hostname
59
+ }
60
+ const error = new Error(
61
+ `Refusing to request a reserved address: ${hostname}`
62
+ )
63
+ error.code = RESERVED_ADDRESS_CODE
64
+ throw error
65
+ }
66
+ }
67
+
40
68
  const gotOpts = {
41
69
  dnsCache: cacheableLookup,
42
70
  https: { rejectUnauthorized: false },
43
- hooks: { beforeRequest: [userAgentHook, tlsHook] }
71
+ hooks: {
72
+ beforeRequest: [reservedAddressHook, userAgentHook, tlsHook],
73
+ beforeRedirect: [reservedAddressHook]
74
+ }
44
75
  }
45
76
 
46
77
  const instance = got.extend(gotOpts)
@@ -2,16 +2,16 @@
2
2
 
3
3
  const ip = require('ipaddr.js')
4
4
 
5
+ const unbracket = hostname =>
6
+ hostname.startsWith('[') && hostname.endsWith(']')
7
+ ? hostname.slice(1, -1)
8
+ : hostname
9
+
5
10
  module.exports = ({ cacheableLookup }) => {
6
11
  const getIpAddress = async hostname => {
7
12
  if (ip.IPv4.isIPv4(hostname)) return hostname
8
- if (
9
- hostname.startsWith('[') &&
10
- hostname.endsWith(']') &&
11
- ip.IPv6.isIPv6(hostname.slice(1, -1))
12
- ) {
13
- return hostname.slice(1, -1)
14
- }
13
+ const literal = unbracket(hostname)
14
+ if (ip.IPv6.isIPv6(literal)) return literal
15
15
  const { address } = await cacheableLookup.lookupAsync(hostname)
16
16
  return address
17
17
  }
@@ -21,3 +21,5 @@ module.exports = ({ cacheableLookup }) => {
21
21
  return ip.process(ipAddress).range() !== 'unicast'
22
22
  }
23
23
  }
24
+
25
+ module.exports.RESERVED_ADDRESS_CODE = 'ERESERVEDADDRESSRANGE'
@@ -7,11 +7,12 @@ module.exports = ({ got, pingCache }) => {
7
7
  value: ({ url, statusCode }) => ({ url, statusCode })
8
8
  })
9
9
 
10
- const reachableUrl = (url, opts) =>
11
- pingUrl(url, {
12
- ...got.gotOpts,
13
- ...opts
14
- })
10
+ const reachableUrl = async (url, opts) => {
11
+ const context = {}
12
+ const response = await pingUrl(url, { ...got.gotOpts, ...opts, context })
13
+ const { reservedAddress } = context
14
+ return reservedAddress ? { ...response, reservedAddress } : response
15
+ }
15
16
 
16
17
  reachableUrl.isReachable = createPingUrl.isReachable
17
18