Compare commits

...

2 commits

Author SHA1 Message Date
Yarmo Mackenbach
334446cee2
feat: Add markers to service provider configs 2023-03-29 13:05:56 +02:00
Yarmo Mackenbach
870a544550
feat: Add marker fetching logic 2023-03-29 13:04:56 +02:00
28 changed files with 201 additions and 38 deletions

View file

@ -16,7 +16,7 @@ limitations under the License.
const validator = require('validator') const validator = require('validator')
const validUrl = require('valid-url') const validUrl = require('valid-url')
const mergeOptions = require('merge-options') const mergeOptions = require('merge-options')
const proofs = require('./proofs') const request = require('./request')
const verifications = require('./verifications') const verifications = require('./verifications')
const claimDefinitions = require('./claimDefinitions') const claimDefinitions = require('./claimDefinitions')
const defaults = require('./defaults') const defaults = require('./defaults')
@ -229,10 +229,44 @@ class Claim {
let verificationResult = null let verificationResult = null
let proofData = null let proofData = null
let markersData = null
let proofFetchError let proofFetchError
// Handle markers
try { try {
proofData = await proofs.fetch(claimData, opts) markersData = await request.fetchMarkers(claimData, opts)
} catch (err) {
proofFetchError = err
}
if (markersData) {
let shouldSkipMatch = false
markersData.forEach(marker => {
// Skip marker if another was already proven false
if (shouldSkipMatch) return
// Ignore markers that were rejected
if (marker.status !== 'fulfilled') return
let endpointExists
switch (marker.value.data.test.type) {
case E.MarkerTestType.HTTP_ENDPOINT_MUST_EXIST:
endpointExists = marker.value.result && !marker.value.error
if ((endpointExists && marker.value.data.test.inverse) || (!(endpointExists || marker.value.data.test.inverse))) {
shouldSkipMatch = true
}
break
default:
break
}
})
if (shouldSkipMatch) continue
}
// Handle proof
try {
proofData = await request.fetchProof(claimData, opts)
} catch (err) { } catch (err) {
proofFetchError = err proofFetchError = err
} }

View file

@ -32,6 +32,7 @@ const processURI = (uri) => {
uri: uri, uri: uri,
qr: null qr: null
}, },
markers: [],
proof: { proof: {
uri: uri, uri: uri,
request: { request: {

View file

@ -29,6 +29,7 @@ const processURI = (uri) => {
regularExpression: reURI, regularExpression: reURI,
isAmbiguous: true isAmbiguous: true
}, },
markers: [],
profile: { profile: {
display: `${match[2]}@${match[1]}`, display: `${match[2]}@${match[1]}`,
uri: uri, uri: uri,

View file

@ -34,6 +34,7 @@ const processURI = (uri) => {
uri: `https://${match[1]}`, uri: `https://${match[1]}`,
qr: null qr: null
}, },
markers: [],
proof: { proof: {
uri: null, uri: null,
request: { request: {

View file

@ -34,6 +34,7 @@ const processURI = (uri) => {
uri: `https://${match[1]}/${match[2]}`, uri: `https://${match[1]}/${match[2]}`,
qr: null qr: null
}, },
markers: [],
proof: { proof: {
uri: uri, uri: uri,
request: { request: {

View file

@ -34,6 +34,38 @@ const processURI = (uri) => {
uri: `https://${match[1]}/${match[2]}`, uri: `https://${match[1]}/${match[2]}`,
qr: null qr: null
}, },
markers: [
{
request: {
fetcher: E.Fetcher.HTTP,
access: E.ProofAccess.NOCORS,
format: E.ProofFormat.JSON,
data: {
url: `https://${match[1]}/api/v1/version`,
format: E.ProofFormat.JSON
}
},
test: {
type: E.MarkerTestType.HTTP_ENDPOINT_MUST_EXIST,
inverse: false
}
},
{
request: {
fetcher: E.Fetcher.HTTP,
access: E.ProofAccess.NOCORS,
format: E.ProofFormat.JSON,
data: {
url: `https://${match[1]}/api/forgejo/v1/version`,
format: E.ProofFormat.JSON
}
},
test: {
type: E.MarkerTestType.HTTP_ENDPOINT_MUST_EXIST,
inverse: false
}
}
],
proof: { proof: {
uri: uri, uri: uri,
request: { request: {

View file

@ -34,6 +34,23 @@ const processURI = (uri) => {
uri: `https://${match[1]}/${match[2]}`, uri: `https://${match[1]}/${match[2]}`,
qr: null qr: null
}, },
markers: [
{
request: {
fetcher: E.Fetcher.HTTP,
access: E.ProofAccess.NOCORS,
format: E.ProofFormat.JSON,
data: {
url: `https://${match[1]}/api/v1/version`,
format: E.ProofFormat.JSON
}
},
test: {
type: E.MarkerTestType.HTTP_ENDPOINT_MUST_EXIST,
inverse: false
}
}
],
proof: { proof: {
uri: uri, uri: uri,
request: { request: {

View file

@ -34,6 +34,7 @@ const processURI = (uri) => {
uri: `https://github.com/${match[1]}`, uri: `https://github.com/${match[1]}`,
qr: null qr: null
}, },
markers: [],
proof: { proof: {
uri: uri, uri: uri,
request: { request: {

View file

@ -34,6 +34,7 @@ const processURI = (uri) => {
uri: `https://${match[1]}/${match[2]}`, uri: `https://${match[1]}/${match[2]}`,
qr: null qr: null
}, },
markers: [],
proof: { proof: {
uri: uri, uri: uri,
request: { request: {

View file

@ -34,6 +34,7 @@ const processURI = (uri) => {
uri: uri, uri: uri,
qr: null qr: null
}, },
markers: [],
proof: { proof: {
uri: `https://hacker-news.firebaseio.com/v0/user/${match[1]}.json`, uri: `https://hacker-news.firebaseio.com/v0/user/${match[1]}.json`,
request: { request: {

View file

@ -27,7 +27,7 @@ const data = {
hackernews: require('./hackernews'), hackernews: require('./hackernews'),
lobsters: require('./lobsters'), lobsters: require('./lobsters'),
forem: require('./forem'), forem: require('./forem'),
// forgejo: require('./forgejo'), forgejo: require('./forgejo'),
gitea: require('./gitea'), gitea: require('./gitea'),
gitlab: require('./gitlab'), gitlab: require('./gitlab'),
github: require('./github'), github: require('./github'),

View file

@ -34,6 +34,7 @@ const processURI = (uri) => {
uri: uri, uri: uri,
qr: null qr: null
}, },
markers: [],
proof: { proof: {
uri: null, uri: null,
request: { request: {

View file

@ -34,6 +34,7 @@ const processURI = (uri) => {
uri: uri, uri: uri,
qr: null qr: null
}, },
markers: [],
proof: { proof: {
uri: `https://keybase.io/_/api/1.0/user/lookup.json?username=${match[1]}`, uri: `https://keybase.io/_/api/1.0/user/lookup.json?username=${match[1]}`,
request: { request: {

View file

@ -34,6 +34,7 @@ const processURI = (uri) => {
uri: uri, uri: uri,
qr: null qr: null
}, },
markers: [],
proof: { proof: {
uri: uri, uri: uri,
request: { request: {

View file

@ -34,6 +34,7 @@ const processURI = (uri) => {
uri: uri, uri: uri,
qr: null qr: null
}, },
markers: [],
proof: { proof: {
uri: `https://lichess.org/api/user/${match[1]}`, uri: `https://lichess.org/api/user/${match[1]}`,
request: { request: {

View file

@ -34,6 +34,7 @@ const processURI = (uri) => {
uri: uri, uri: uri,
qr: null qr: null
}, },
markers: [],
proof: { proof: {
uri: `https://lobste.rs/u/${match[1]}.json`, uri: `https://lobste.rs/u/${match[1]}.json`,
request: { request: {

View file

@ -50,6 +50,7 @@ const processURI = (uri) => {
uri: profileUrl, uri: profileUrl,
qr: null qr: null
}, },
markers: [],
proof: { proof: {
uri: eventUrl, uri: eventUrl,
request: { request: {

View file

@ -34,6 +34,7 @@ const processURI = (uri) => {
uri: uri, uri: uri,
qr: null qr: null
}, },
markers: [],
proof: { proof: {
uri: uri, uri: uri,
request: { request: {

View file

@ -34,6 +34,7 @@ const processURI = (uri) => {
uri: uri, uri: uri,
qr: null qr: null
}, },
markers: [],
proof: { proof: {
uri: `${uri}/api/config`, uri: `${uri}/api/config`,
request: { request: {

View file

@ -34,6 +34,7 @@ const processURI = (uri) => {
uri: `https://www.reddit.com/user/${match[1]}`, uri: `https://www.reddit.com/user/${match[1]}`,
qr: null qr: null
}, },
markers: [],
proof: { proof: {
uri: uri, uri: uri,
request: { request: {

View file

@ -36,6 +36,7 @@ const processURI = (uri) => {
uri: uri, uri: uri,
qr: null qr: null
}, },
markers: [],
proof: { proof: {
uri: `https://${domain}.com/users/${id}?tab=profile`, uri: `https://${domain}.com/users/${id}?tab=profile`,
request: { request: {

View file

@ -34,6 +34,7 @@ const processURI = (uri) => {
uri: `https://t.me/${match[1]}`, uri: `https://t.me/${match[1]}`,
qr: `https://t.me/${match[1]}` qr: `https://t.me/${match[1]}`
}, },
markers: [],
proof: { proof: {
uri: `https://t.me/${match[2]}`, uri: `https://t.me/${match[2]}`,
request: { request: {

View file

@ -34,6 +34,7 @@ const processURI = (uri) => {
uri: `https://twitter.com/${match[1]}`, uri: `https://twitter.com/${match[1]}`,
qr: null qr: null
}, },
markers: [],
proof: { proof: {
uri: uri, uri: uri,
request: { request: {

View file

@ -34,6 +34,7 @@ const processURI = (uri) => {
uri: uri, uri: uri,
qr: uri qr: uri
}, },
markers: [],
proof: { proof: {
uri: null, uri: null,
request: { request: {

View file

@ -146,6 +146,20 @@ const ClaimStatus = {
} }
Object.freeze(ClaimStatus) Object.freeze(ClaimStatus)
/**
* How to test a marker
* @readonly
* @enum {string}
*/
const MarkerTestType = {
/** HTTP endpoint must exist */
HTTP_ENDPOINT_MUST_EXIST: 'httpEndpointMustExist'
// TODO Implement JSON_CONTAINS
// /** JSON data must contain a certain string */
// JSON_CONTAINS: 'jsonContains'
}
Object.freeze(MarkerTestType)
exports.ProxyPolicy = ProxyPolicy exports.ProxyPolicy = ProxyPolicy
exports.Fetcher = Fetcher exports.Fetcher = Fetcher
exports.EntityEncodingFormat = EntityEncodingFormat exports.EntityEncodingFormat = EntityEncodingFormat
@ -154,3 +168,4 @@ exports.ProofFormat = ProofFormat
exports.ClaimFormat = ClaimFormat exports.ClaimFormat = ClaimFormat
exports.ClaimRelation = ClaimRelation exports.ClaimRelation = ClaimRelation
exports.ClaimStatus = ClaimStatus exports.ClaimStatus = ClaimStatus
exports.MarkerTestType = MarkerTestType

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
const Claim = require('./claim') const Claim = require('./claim')
const claimDefinitions = require('./claimDefinitions') const claimDefinitions = require('./claimDefinitions')
const proofs = require('./proofs') const request = require('./request')
const keys = require('./keys') const keys = require('./keys')
const signatures = require('./signatures') const signatures = require('./signatures')
const enums = require('./enums') const enums = require('./enums')
@ -26,7 +26,7 @@ const fetcher = require('./fetcher')
exports.Claim = Claim exports.Claim = Claim
exports.claimDefinitions = claimDefinitions exports.claimDefinitions = claimDefinitions
exports.proofs = proofs exports.request = request
exports.keys = keys exports.keys = keys
exports.signatures = signatures exports.signatures = signatures
exports.enums = enums exports.enums = enums

View file

@ -19,7 +19,7 @@ const utils = require('./utils')
const E = require('./enums') const E = require('./enums')
/** /**
* @module proofs * @module request
*/ */
/** /**
@ -33,7 +33,7 @@ const E = require('./enums')
* @param {object} opts - Options to enable the request * @param {object} opts - Options to enable the request
* @returns {Promise<object|string>} * @returns {Promise<object|string>}
*/ */
const fetch = (data, opts) => { const fetchProof = (data, opts) => {
switch (data.proof.request.fetcher) { switch (data.proof.request.fetcher) {
case E.Fetcher.HTTP: case E.Fetcher.HTTP:
data.proof.request.data.format = data.proof.request.format data.proof.request.data.format = data.proof.request.format
@ -44,22 +44,49 @@ const fetch = (data, opts) => {
} }
if (jsEnv.isNode) { if (jsEnv.isNode) {
return handleNodeRequests(data, opts) return handleNodeRequests(data.proof, opts, false)
} }
return handleBrowserRequests(data, opts) return handleBrowserRequests(data.proof, opts, false)
} }
const handleBrowserRequests = (data, opts) => { /**
* Delegate the marker requests to the correct fetcher.
* This method uses the current environment (browser/node), certain values from
* the `data` parameter and the proxy policy set in the `opts` parameter to
* choose the right approach to fetch the proof. An error will be thrown if no
* approach is possible.
* @async
* @param {object} data - Data from a claim definition
* @param {object} opts - Options to enable the request
* @returns {Promise<Array<object>>}
*/
const fetchMarkers = async (data, opts) => {
const promises = []
if (!(data.markers && data.markers.length > 0)) throw new Error('No markers found')
data.markers.forEach(marker => {
if (jsEnv.isNode) {
promises.push(handleNodeRequests(marker, opts, true))
} else {
promises.push(handleBrowserRequests(marker, opts, true))
}
})
return Promise.allSettled(promises)
}
const handleBrowserRequests = (data, opts, alwaysResolve) => {
switch (opts.proxy.policy) { switch (opts.proxy.policy) {
case E.ProxyPolicy.ALWAYS: case E.ProxyPolicy.ALWAYS:
return createProxyRequestPromise(data, opts) return createProxyRequestPromise(data, opts, alwaysResolve)
case E.ProxyPolicy.NEVER: case E.ProxyPolicy.NEVER:
switch (data.proof.request.access) { switch (data.request.access) {
case E.ProofAccess.GENERIC: case E.ProofAccess.GENERIC:
case E.ProofAccess.GRANTED: case E.ProofAccess.GRANTED:
return createDefaultRequestPromise(data, opts) return createDefaultRequestPromise(data, opts, alwaysResolve)
case E.ProofAccess.NOCORS: case E.ProofAccess.NOCORS:
case E.ProofAccess.SERVER: case E.ProofAccess.SERVER:
throw new Error( throw new Error(
@ -70,15 +97,13 @@ const handleBrowserRequests = (data, opts) => {
} }
case E.ProxyPolicy.ADAPTIVE: case E.ProxyPolicy.ADAPTIVE:
switch (data.proof.request.access) { switch (data.request.access) {
case E.ProofAccess.GENERIC: case E.ProofAccess.GENERIC:
return createFallbackRequestPromise(data, opts)
case E.ProofAccess.NOCORS:
return createProxyRequestPromise(data, opts)
case E.ProofAccess.GRANTED: case E.ProofAccess.GRANTED:
return createFallbackRequestPromise(data, opts) return createFallbackRequestPromise(data, opts, alwaysResolve)
case E.ProofAccess.NOCORS:
case E.ProofAccess.SERVER: case E.ProofAccess.SERVER:
return createProxyRequestPromise(data, opts) return createProxyRequestPromise(data, opts, alwaysResolve)
default: default:
throw new Error('Invalid proof access value') throw new Error('Invalid proof access value')
} }
@ -88,47 +113,56 @@ const handleBrowserRequests = (data, opts) => {
} }
} }
const handleNodeRequests = (data, opts) => { const handleNodeRequests = (data, opts, alwaysResolve) => {
switch (opts.proxy.policy) { switch (opts.proxy.policy) {
case E.ProxyPolicy.ALWAYS: case E.ProxyPolicy.ALWAYS:
return createProxyRequestPromise(data, opts) return createProxyRequestPromise(data, opts, alwaysResolve)
case E.ProxyPolicy.NEVER: case E.ProxyPolicy.NEVER:
return createDefaultRequestPromise(data, opts) return createDefaultRequestPromise(data, opts, alwaysResolve)
case E.ProxyPolicy.ADAPTIVE: case E.ProxyPolicy.ADAPTIVE:
return createFallbackRequestPromise(data, opts) return createFallbackRequestPromise(data, opts, alwaysResolve)
default: default:
throw new Error('Invalid proxy policy') throw new Error('Invalid proxy policy')
} }
} }
const createDefaultRequestPromise = (data, opts) => { const createDefaultRequestPromise = (data, opts, alwaysResolve) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fetcher[data.proof.request.fetcher] fetcher[data.request.fetcher]
.fn(data.proof.request.data, opts) .fn(data.request.data, opts)
.then((res) => { .then((res) => {
return resolve({ return resolve({
fetcher: data.proof.request.fetcher, fetcher: data.request.fetcher,
data: data, data: data,
viaProxy: false, viaProxy: false,
result: res result: res
}) })
}) })
.catch((err) => { .catch((err) => {
return reject(err) if (alwaysResolve) {
return resolve({
fetcher: 'http',
data: data,
viaProxy: true,
error: err
})
} else {
return reject(err)
}
}) })
}) })
} }
const createProxyRequestPromise = (data, opts) => { const createProxyRequestPromise = (data, opts, alwaysResolve) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let proxyUrl let proxyUrl
try { try {
proxyUrl = utils.generateProxyURL( proxyUrl = utils.generateProxyURL(
data.proof.request.fetcher, data.request.fetcher,
data.proof.request.data, data.request.data,
opts opts
) )
} catch (err) { } catch (err) {
@ -137,8 +171,8 @@ const createProxyRequestPromise = (data, opts) => {
const requestData = { const requestData = {
url: proxyUrl, url: proxyUrl,
format: data.proof.request.format, format: data.request.format,
fetcherTimeout: fetcher[data.proof.request.fetcher].timeout fetcherTimeout: fetcher[data.request.fetcher].timeout
} }
fetcher.http fetcher.http
.fn(requestData, opts) .fn(requestData, opts)
@ -151,19 +185,28 @@ const createProxyRequestPromise = (data, opts) => {
}) })
}) })
.catch((err) => { .catch((err) => {
return reject(err) if (alwaysResolve) {
return resolve({
fetcher: 'http',
data: data,
viaProxy: true,
error: err
})
} else {
return reject(err)
}
}) })
}) })
} }
const createFallbackRequestPromise = (data, opts) => { const createFallbackRequestPromise = (data, opts, alwaysResolve) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
createDefaultRequestPromise(data, opts) createDefaultRequestPromise(data, opts, alwaysResolve)
.then((res) => { .then((res) => {
return resolve(res) return resolve(res)
}) })
.catch((err1) => { .catch((err1) => {
createProxyRequestPromise(data, opts) createProxyRequestPromise(data, opts, alwaysResolve)
.then((res) => { .then((res) => {
return resolve(res) return resolve(res)
}) })
@ -174,4 +217,5 @@ const createFallbackRequestPromise = (data, opts) => {
}) })
} }
exports.fetch = fetch exports.fetchProof = fetchProof
exports.fetchMarkers = fetchMarkers

View file

@ -38,6 +38,7 @@ const pattern = {
return _.isString(x) || _.isNull(x) return _.isString(x) || _.isNull(x)
}, },
}, },
markers: _.isArray,
proof: { proof: {
uri: (x) => { uri: (x) => {
return _.isString(x) || _.isNull(x) return _.isString(x) || _.isNull(x)