mirror of
https://codeberg.org/keyoxide/keyoxide-web.git
synced 2024-12-22 23:09:29 -07:00
feat: introduce API v3, obsolete older versions
This commit is contained in:
parent
3ff82ff46d
commit
6676c78961
6 changed files with 186 additions and 816 deletions
|
@ -1,399 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright (C) 2021 Yarmo Mackenbach
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify it under
|
|
||||||
the terms of the GNU Affero General Public License as published by the Free
|
|
||||||
Software Foundation, either version 3 of the License, or (at your option)
|
|
||||||
any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
||||||
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
||||||
details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License along
|
|
||||||
with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If your software can interact with users remotely through a computer network,
|
|
||||||
you should also make sure that it provides a way for users to get its source.
|
|
||||||
For example, if your program is a web application, its interface could display
|
|
||||||
a "Source" link that leads users to an archive of the code. There are many
|
|
||||||
ways you could offer source, and different solutions will be better for different
|
|
||||||
programs; see section 13 for the specific requirements.
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary. For
|
|
||||||
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
import express from 'express'
|
|
||||||
import { check, validationResult } from 'express-validator'
|
|
||||||
import Ajv from 'ajv'
|
|
||||||
import { generateWKDProfile, generateHKPProfile, generateAutoProfile } from '../../server/index.js'
|
|
||||||
import * as dotenv from 'dotenv'
|
|
||||||
dotenv.config()
|
|
||||||
|
|
||||||
const router = express.Router()
|
|
||||||
const ajv = new Ajv({ coerceTypes: true })
|
|
||||||
|
|
||||||
const apiProfileSchema = {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
keyData: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
fingerprint: {
|
|
||||||
type: 'string'
|
|
||||||
},
|
|
||||||
openpgp4fpr: {
|
|
||||||
type: 'string'
|
|
||||||
},
|
|
||||||
users: {
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
userData: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
id: { type: 'string' },
|
|
||||||
name: { type: 'string' },
|
|
||||||
email: { type: 'string' },
|
|
||||||
comment: { type: 'string' },
|
|
||||||
isPrimary: { type: 'boolean' },
|
|
||||||
isRevoked: { type: 'boolean' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
claims: {
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
claimVersion: { type: 'integer' },
|
|
||||||
uri: { type: 'string' },
|
|
||||||
fingerprint: { type: 'string' },
|
|
||||||
status: { type: 'string' },
|
|
||||||
matches: {
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
serviceProvider: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
type: { type: 'string' },
|
|
||||||
name: { type: 'string' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
match: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
regularExpression: { type: 'object' },
|
|
||||||
isAmbiguous: { type: 'boolean' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
profile: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
display: { type: 'string' },
|
|
||||||
uri: { type: 'string' },
|
|
||||||
qr: { type: 'string' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
proof: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
uri: { type: 'string' },
|
|
||||||
request: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
fetcher: { type: 'string' },
|
|
||||||
access: { type: 'string' },
|
|
||||||
format: { type: 'string' },
|
|
||||||
data: { type: 'object' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
claim: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
format: { type: 'string' },
|
|
||||||
relation: { type: 'string' },
|
|
||||||
path: {
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
type: 'string'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
verification: {
|
|
||||||
type: 'object'
|
|
||||||
},
|
|
||||||
summary: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
profileName: { type: 'string' },
|
|
||||||
profileURL: { type: 'string' },
|
|
||||||
serviceProviderName: { type: 'string' },
|
|
||||||
isVerificationDone: { type: 'boolean' },
|
|
||||||
isVerified: { type: 'boolean' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
primaryUserIndex: {
|
|
||||||
type: 'integer'
|
|
||||||
},
|
|
||||||
key: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
data: { type: 'object' },
|
|
||||||
fetchMethod: { type: 'string' },
|
|
||||||
uri: { type: 'string' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
keyoxide: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
url: { type: 'string' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
extra: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
avatarURL: { type: 'string' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
errors: {
|
|
||||||
type: 'array'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
required: ['keyData', 'keyoxide', 'extra', 'errors'],
|
|
||||||
additionalProperties: false
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiProfileValidate = ajv.compile(apiProfileSchema)
|
|
||||||
|
|
||||||
const doVerification = async (data) => {
|
|
||||||
const promises = []
|
|
||||||
const results = []
|
|
||||||
const verificationOptions = {
|
|
||||||
proxy: {
|
|
||||||
hostname: process.env.PROXY_HOSTNAME,
|
|
||||||
policy: (process.env.PROXY_HOSTNAME !== '') ? 'adaptive' : 'never'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return early if no users in key
|
|
||||||
if (!data.keyData.users) {
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let iUser = 0; iUser < data.keyData.users.length; iUser++) {
|
|
||||||
const user = data.keyData.users[iUser]
|
|
||||||
|
|
||||||
for (let iClaim = 0; iClaim < user.claims.length; iClaim++) {
|
|
||||||
const claim = user.claims[iClaim]
|
|
||||||
|
|
||||||
promises.push(
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
(async () => {
|
|
||||||
await claim.verify(verificationOptions)
|
|
||||||
results.push([iUser, iClaim, claim])
|
|
||||||
resolve()
|
|
||||||
})()
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await Promise.all(promises)
|
|
||||||
|
|
||||||
results.forEach(result => {
|
|
||||||
data.keyData.users[result[0]].claims[result[1]] = result[2]
|
|
||||||
})
|
|
||||||
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
const sanitize = (data) => {
|
|
||||||
const dataClone = JSON.parse(JSON.stringify(data))
|
|
||||||
|
|
||||||
if (dataClone.keyData.users) {
|
|
||||||
for (let iUser = 0; iUser < dataClone.keyData.users.length; iUser++) {
|
|
||||||
const user = dataClone.keyData.users[iUser]
|
|
||||||
|
|
||||||
for (let iClaim = 0; iClaim < user.claims.length; iClaim++) {
|
|
||||||
const claim = user.claims[iClaim]
|
|
||||||
|
|
||||||
// TODO Fix upstream
|
|
||||||
for (let iMatch = 0; iMatch < claim.matches.length; iMatch++) {
|
|
||||||
const match = claim.matches[iMatch]
|
|
||||||
if (Array.isArray(match.claim)) {
|
|
||||||
match.claim = match.claim[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO Fix upstream
|
|
||||||
if (!claim.verification) {
|
|
||||||
claim.verification = {}
|
|
||||||
}
|
|
||||||
// TODO Fix upstream
|
|
||||||
claim.matches.forEach(match => {
|
|
||||||
match.proof.request.access = ['generic', 'nocors', 'granted', 'server'][match.proof.request.access]
|
|
||||||
match.claim.format = ['uri', 'fingerprint', 'message'][match.claim.format]
|
|
||||||
match.claim.relation = ['contains', 'equals', 'oneof'][match.claim.relation]
|
|
||||||
})
|
|
||||||
|
|
||||||
data.keyData.users[iUser].claims[iClaim] = claim
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const valid = apiProfileValidate(data)
|
|
||||||
if (!valid) {
|
|
||||||
throw new Error('Profile data sanitization error')
|
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
const addSummaryToClaims = (data) => {
|
|
||||||
// Return early if no users in key
|
|
||||||
if (!data.keyData.users) {
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// To be removed when data is added by DOIP library
|
|
||||||
for (let userIndex = 0; userIndex < data.keyData.users.length; userIndex++) {
|
|
||||||
const user = data.keyData.users[userIndex]
|
|
||||||
|
|
||||||
for (let claimIndex = 0; claimIndex < user.claims.length; claimIndex++) {
|
|
||||||
const claim = user.claims[claimIndex]
|
|
||||||
|
|
||||||
const isVerificationDone = claim.status === 'verified'
|
|
||||||
const isVerified = isVerificationDone ? claim.verification.result : false
|
|
||||||
const isAmbiguous = isVerified
|
|
||||||
? false
|
|
||||||
: claim.matches.length > 1 || claim.matches[0].match.isAmbiguous
|
|
||||||
|
|
||||||
data.keyData.users[userIndex].claims[claimIndex].summary = {
|
|
||||||
profileName: !isAmbiguous ? claim.matches[0].profile.display : claim.uri,
|
|
||||||
profileURL: !isAmbiguous ? claim.matches[0].profile.uri : '',
|
|
||||||
serviceProviderName: !isAmbiguous ? claim.matches[0].serviceprovider.name : '',
|
|
||||||
isVerificationDone,
|
|
||||||
isVerified
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
router.get('/profile/fetch',
|
|
||||||
check('query').exists(),
|
|
||||||
check('protocol').optional().toLowerCase().isIn(['hkp', 'wkd']),
|
|
||||||
check('doVerification').default(false).isBoolean().toBoolean(),
|
|
||||||
check('returnPublicKey').default(false).isBoolean().toBoolean(),
|
|
||||||
async (req, res) => {
|
|
||||||
const valRes = validationResult(req)
|
|
||||||
if (!valRes.isEmpty()) {
|
|
||||||
res.status(400).send(valRes)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate profile
|
|
||||||
let data
|
|
||||||
switch (req.query.protocol) {
|
|
||||||
case 'wkd':
|
|
||||||
data = await generateWKDProfile(req.query.query)
|
|
||||||
break
|
|
||||||
case 'hkp':
|
|
||||||
data = await generateHKPProfile(req.query.query)
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
data = await generateAutoProfile(req.query.query)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.errors.length > 0) {
|
|
||||||
data.key = undefined
|
|
||||||
res.status(500).send(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return public key
|
|
||||||
if (req.query.returnPublicKey) {
|
|
||||||
data.keyData.key.data = data.key.publicKey
|
|
||||||
}
|
|
||||||
data.key = undefined
|
|
||||||
|
|
||||||
// Do verification
|
|
||||||
if (req.query.doVerification) {
|
|
||||||
data = await doVerification(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Sanitize JSON
|
|
||||||
data = sanitize(data)
|
|
||||||
} catch (error) {
|
|
||||||
data.keyData = {}
|
|
||||||
data.extra = {}
|
|
||||||
data.errors = [error.message]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add missing data
|
|
||||||
data = addSummaryToClaims(data)
|
|
||||||
|
|
||||||
let statusCode = 200
|
|
||||||
if (data.errors.length > 0) {
|
|
||||||
statusCode = 500
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(statusCode).send(data)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
router.get('/profile/verify',
|
|
||||||
check('data').exists().isJSON(),
|
|
||||||
async (req, res) => {
|
|
||||||
const valRes = validationResult(req)
|
|
||||||
if (!valRes.isEmpty()) {
|
|
||||||
res.status(400).send(valRes)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do verification
|
|
||||||
let data = await doVerification(req.query.data)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Sanitize JSON
|
|
||||||
data = sanitize(data)
|
|
||||||
} catch (error) {
|
|
||||||
data.keyData = {}
|
|
||||||
data.extra = {}
|
|
||||||
data.errors = [error.message]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add missing data
|
|
||||||
data = addSummaryToClaims(data)
|
|
||||||
|
|
||||||
let statusCode = 200
|
|
||||||
if (data.errors.length > 0) {
|
|
||||||
statusCode = 500
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(statusCode).send(data)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export default router
|
|
|
@ -1,23 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2022 Yarmo Mackenbach
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
import express from 'express'
|
|
||||||
const router = express.Router()
|
|
||||||
|
|
||||||
router.get('*', (req, res) => {
|
|
||||||
return res.status(501).send('Proxy v1 API endpoint is no longer supported, please migrate to proxy v2 API endpoint')
|
|
||||||
})
|
|
||||||
|
|
||||||
export default router
|
|
|
@ -1,370 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright (C) 2022 Yarmo Mackenbach
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify it under
|
|
||||||
the terms of the GNU Affero General Public License as published by the Free
|
|
||||||
Software Foundation, either version 3 of the License, or (at your option)
|
|
||||||
any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
||||||
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
||||||
details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License along
|
|
||||||
with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If your software can interact with users remotely through a computer network,
|
|
||||||
you should also make sure that it provides a way for users to get its source.
|
|
||||||
For example, if your program is a web application, its interface could display
|
|
||||||
a "Source" link that leads users to an archive of the code. There are many
|
|
||||||
ways you could offer source, and different solutions will be better for different
|
|
||||||
programs; see section 13 for the specific requirements.
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary. For
|
|
||||||
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
import express from 'express'
|
|
||||||
import { check, validationResult } from 'express-validator'
|
|
||||||
import Ajv from 'ajv'
|
|
||||||
import { generateWKDProfile, generateHKPProfile, generateAutoProfile } from '../../server/index.js'
|
|
||||||
import * as dotenv from 'dotenv'
|
|
||||||
dotenv.config()
|
|
||||||
|
|
||||||
const router = express.Router()
|
|
||||||
const ajv = new Ajv({ coerceTypes: true })
|
|
||||||
|
|
||||||
const apiProfileSchema = {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
keyData: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
fingerprint: {
|
|
||||||
type: 'string'
|
|
||||||
},
|
|
||||||
openpgp4fpr: {
|
|
||||||
type: 'string'
|
|
||||||
},
|
|
||||||
users: {
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
userData: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
id: { type: 'string' },
|
|
||||||
name: { type: 'string' },
|
|
||||||
email: { type: 'string' },
|
|
||||||
comment: { type: 'string' },
|
|
||||||
isPrimary: { type: 'boolean' },
|
|
||||||
isRevoked: { type: 'boolean' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
claims: {
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
claimVersion: { type: 'integer' },
|
|
||||||
uri: { type: 'string' },
|
|
||||||
fingerprint: { type: 'string' },
|
|
||||||
status: { type: 'string' },
|
|
||||||
matches: {
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
serviceProvider: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
type: { type: 'string' },
|
|
||||||
name: { type: 'string' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
match: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
regularExpression: { type: 'object' },
|
|
||||||
isAmbiguous: { type: 'boolean' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
profile: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
display: { type: 'string' },
|
|
||||||
uri: { type: 'string' },
|
|
||||||
qr: { type: 'string' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
proof: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
uri: { type: 'string' },
|
|
||||||
request: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
fetcher: { type: 'string' },
|
|
||||||
access: { type: 'string' },
|
|
||||||
format: { type: 'string' },
|
|
||||||
data: { type: 'object' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
claim: {
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
format: { type: 'string' },
|
|
||||||
relation: { type: 'string' },
|
|
||||||
path: {
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
type: 'string'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
verification: {
|
|
||||||
type: 'object'
|
|
||||||
},
|
|
||||||
summary: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
profileName: { type: 'string' },
|
|
||||||
profileURL: { type: 'string' },
|
|
||||||
serviceProviderName: { type: 'string' },
|
|
||||||
isVerificationDone: { type: 'boolean' },
|
|
||||||
isVerified: { type: 'boolean' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
primaryUserIndex: {
|
|
||||||
type: 'integer'
|
|
||||||
},
|
|
||||||
key: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
data: { type: 'object' },
|
|
||||||
fetchMethod: { type: 'string' },
|
|
||||||
uri: { type: 'string' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
keyoxide: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
url: { type: 'string' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
extra: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
avatarURL: { type: 'string' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
errors: {
|
|
||||||
type: 'array'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
required: ['keyData', 'keyoxide', 'extra', 'errors'],
|
|
||||||
additionalProperties: false
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiProfileValidate = ajv.compile(apiProfileSchema)
|
|
||||||
|
|
||||||
const doVerification = async (data) => {
|
|
||||||
const promises = []
|
|
||||||
const results = []
|
|
||||||
const verificationOptions = {
|
|
||||||
proxy: {
|
|
||||||
hostname: process.env.PROXY_HOSTNAME,
|
|
||||||
policy: (process.env.PROXY_HOSTNAME !== '') ? 'adaptive' : 'never'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return early if no users in key
|
|
||||||
if (!data.keyData.users) {
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let iUser = 0; iUser < data.keyData.users.length; iUser++) {
|
|
||||||
const user = data.keyData.users[iUser]
|
|
||||||
|
|
||||||
for (let iClaim = 0; iClaim < user.claims.length; iClaim++) {
|
|
||||||
const claim = user.claims[iClaim]
|
|
||||||
|
|
||||||
promises.push(
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
(async () => {
|
|
||||||
await claim.verify(verificationOptions)
|
|
||||||
results.push([iUser, iClaim, claim])
|
|
||||||
resolve()
|
|
||||||
})()
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await Promise.all(promises)
|
|
||||||
|
|
||||||
results.forEach(result => {
|
|
||||||
data.keyData.users[result[0]].claims[result[1]] = result[2]
|
|
||||||
})
|
|
||||||
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
const sanitize = (data) => {
|
|
||||||
const valid = apiProfileValidate(data)
|
|
||||||
if (!valid) {
|
|
||||||
throw new Error('Profile data sanitization error')
|
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
const addSummaryToClaims = (data) => {
|
|
||||||
// Return early if no users in key
|
|
||||||
if (!data.keyData.users) {
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// To be removed when data is added by DOIP library
|
|
||||||
for (let userIndex = 0; userIndex < data.keyData.users.length; userIndex++) {
|
|
||||||
const user = data.keyData.users[userIndex]
|
|
||||||
|
|
||||||
for (let claimIndex = 0; claimIndex < user.claims.length; claimIndex++) {
|
|
||||||
const claim = user.claims[claimIndex]
|
|
||||||
|
|
||||||
const isVerificationDone = claim.status === 'verified'
|
|
||||||
const isVerified = isVerificationDone ? claim.verification.result : false
|
|
||||||
const isAmbiguous = isVerified
|
|
||||||
? false
|
|
||||||
: claim.matches.length > 1 || claim.matches[0].match.isAmbiguous
|
|
||||||
|
|
||||||
data.keyData.users[userIndex].claims[claimIndex].summary = {
|
|
||||||
profileName: !isAmbiguous ? claim.matches[0].profile.display : claim.uri,
|
|
||||||
profileURL: !isAmbiguous ? claim.matches[0].profile.uri : '',
|
|
||||||
serviceProviderName: !isAmbiguous ? claim.matches[0].serviceprovider.name : '',
|
|
||||||
isVerificationDone,
|
|
||||||
isVerified
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
router.get('/fetch',
|
|
||||||
check('query').exists(),
|
|
||||||
check('protocol').optional().toLowerCase().isIn(['hkp', 'wkd']),
|
|
||||||
check('doVerification').default(false).isBoolean().toBoolean(),
|
|
||||||
check('returnPublicKey').default(false).isBoolean().toBoolean(),
|
|
||||||
async (req, res) => {
|
|
||||||
const valRes = validationResult(req)
|
|
||||||
if (!valRes.isEmpty()) {
|
|
||||||
res.status(400).send(valRes)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate profile
|
|
||||||
let data
|
|
||||||
switch (req.query.protocol) {
|
|
||||||
case 'wkd':
|
|
||||||
data = await generateWKDProfile(req.query.query)
|
|
||||||
break
|
|
||||||
case 'hkp':
|
|
||||||
data = await generateHKPProfile(req.query.query)
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
data = await generateAutoProfile(req.query.query)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.errors.length > 0) {
|
|
||||||
data.key = undefined
|
|
||||||
res.status(500).send(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return public key
|
|
||||||
if (req.query.returnPublicKey) {
|
|
||||||
data.keyData.key.data = data.key.publicKey
|
|
||||||
}
|
|
||||||
data.key = undefined
|
|
||||||
|
|
||||||
// Do verification
|
|
||||||
if (req.query.doVerification) {
|
|
||||||
data = await doVerification(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Sanitize JSON
|
|
||||||
data = sanitize(data)
|
|
||||||
} catch (error) {
|
|
||||||
data.keyData = {}
|
|
||||||
data.extra = {}
|
|
||||||
data.errors = [error.message]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add missing data
|
|
||||||
data = addSummaryToClaims(data)
|
|
||||||
|
|
||||||
let statusCode = 200
|
|
||||||
if (data.errors.length > 0) {
|
|
||||||
statusCode = 500
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(statusCode).send(data)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
router.get('/verify',
|
|
||||||
check('data').exists().isJSON(),
|
|
||||||
async (req, res) => {
|
|
||||||
const valRes = validationResult(req)
|
|
||||||
if (!valRes.isEmpty()) {
|
|
||||||
res.status(400).send(valRes)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do verification
|
|
||||||
let data = await doVerification(req.query.data)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Sanitize JSON
|
|
||||||
data = sanitize(data)
|
|
||||||
} catch (error) {
|
|
||||||
data.keyData = {}
|
|
||||||
data.extra = {}
|
|
||||||
data.errors = [error.message]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add missing data
|
|
||||||
data = addSummaryToClaims(data)
|
|
||||||
|
|
||||||
let statusCode = 200
|
|
||||||
if (data.errors.length > 0) {
|
|
||||||
statusCode = 500
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(statusCode).send(data)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export default router
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright (C) 2022 Yarmo Mackenbach
|
Copyright (C) 2023 Yarmo Mackenbach
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify it under
|
This program is free software: you can redistribute it and/or modify it under
|
||||||
the terms of the GNU Affero General Public License as published by the Free
|
the terms of the GNU Affero General Public License as published by the Free
|
185
src/api/v3/keyoxide_profile.js
Normal file
185
src/api/v3/keyoxide_profile.js
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
/*
|
||||||
|
Copyright (C) 2023 Yarmo Mackenbach
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify it under
|
||||||
|
the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
Software Foundation, either version 3 of the License, or (at your option)
|
||||||
|
any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License along
|
||||||
|
with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer network,
|
||||||
|
you should also make sure that it provides a way for users to get its source.
|
||||||
|
For example, if your program is a web application, its interface could display
|
||||||
|
a "Source" link that leads users to an archive of the code. There are many
|
||||||
|
ways you could offer source, and different solutions will be better for different
|
||||||
|
programs; see section 13 for the specific requirements.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary. For
|
||||||
|
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import express from 'express'
|
||||||
|
import { check, validationResult } from 'express-validator'
|
||||||
|
import Ajv from 'ajv/dist/2020.js'
|
||||||
|
import * as dotenv from 'dotenv'
|
||||||
|
import { Claim } from 'doipjs'
|
||||||
|
import { generateAspeProfile, generateWKDProfile, generateHKPProfile, generateAutoProfile } from '../../server/index.js'
|
||||||
|
import { claimSchema, personaSchema, profileSchema, serviceProviderSchema } from '../../schemas.js'
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
|
const router = express.Router()
|
||||||
|
const ajv = new Ajv({
|
||||||
|
schemas: [profileSchema, personaSchema, claimSchema, serviceProviderSchema]
|
||||||
|
})
|
||||||
|
|
||||||
|
const apiProfileValidate = ajv.compile(profileSchema)
|
||||||
|
|
||||||
|
const doVerification = async (profile) => {
|
||||||
|
const promises = []
|
||||||
|
const results = []
|
||||||
|
const verificationOptions = {
|
||||||
|
proxy: {
|
||||||
|
hostname: process.env.PROXY_HOSTNAME,
|
||||||
|
policy: (process.env.PROXY_HOSTNAME !== '') ? 'adaptive' : 'never'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return early if no users in key
|
||||||
|
if (!profile.personas) {
|
||||||
|
return profile
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let iUser = 0; iUser < profile.personas.length; iUser++) {
|
||||||
|
const user = profile.personas[iUser]
|
||||||
|
|
||||||
|
for (let iClaim = 0; iClaim < user.claims.length; iClaim++) {
|
||||||
|
const claim = user.claims[iClaim]
|
||||||
|
|
||||||
|
promises.push(
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
(async () => {
|
||||||
|
await claim.verify(verificationOptions)
|
||||||
|
results.push([iUser, iClaim, claim])
|
||||||
|
resolve()
|
||||||
|
})()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Promise.all(promises)
|
||||||
|
|
||||||
|
results.forEach(result => {
|
||||||
|
profile.personas[result[0]].claims[result[1]] = result[2]
|
||||||
|
})
|
||||||
|
|
||||||
|
return profile
|
||||||
|
}
|
||||||
|
|
||||||
|
const validate = (profile) => {
|
||||||
|
const valid = apiProfileValidate(profile)
|
||||||
|
if (!valid) {
|
||||||
|
throw new Error(`Profile data validation error: ${apiProfileValidate.errors.map(x => x.message).join(', ')}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get('/fetch',
|
||||||
|
check('query').exists(),
|
||||||
|
check('protocol').optional().toLowerCase().isIn(['aspe', 'hkp', 'wkd']),
|
||||||
|
check('doVerification').default(false).isBoolean().toBoolean(),
|
||||||
|
async (req, res) => {
|
||||||
|
const valRes = validationResult(req)
|
||||||
|
if (!valRes.isEmpty()) {
|
||||||
|
res.status(400).send(valRes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate profile
|
||||||
|
let data
|
||||||
|
switch (req.query.protocol) {
|
||||||
|
case 'aspe':
|
||||||
|
data = await generateAspeProfile(req.query.query)
|
||||||
|
break
|
||||||
|
case 'wkd':
|
||||||
|
data = await generateWKDProfile(req.query.query)
|
||||||
|
break
|
||||||
|
case 'hkp':
|
||||||
|
data = await generateHKPProfile(req.query.query)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
data = await generateAutoProfile(req.query.query)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('errors' in data && data.errors.length > 0) {
|
||||||
|
res.status(500).send(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do verification
|
||||||
|
if (req.query.doVerification) {
|
||||||
|
data = await doVerification(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
data = data.toJSON()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Validate JSON
|
||||||
|
validate(data)
|
||||||
|
} catch (error) {
|
||||||
|
data = {
|
||||||
|
errors: [error.message]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let statusCode = 200
|
||||||
|
if ('errors' in data && data.errors.length > 0) {
|
||||||
|
statusCode = 500
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(statusCode).send(data)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
router.get('/verify',
|
||||||
|
check('data').exists().isJSON(),
|
||||||
|
async (req, res) => {
|
||||||
|
const valRes = validationResult(req)
|
||||||
|
if (!valRes.isEmpty()) {
|
||||||
|
res.status(400).send(valRes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let profile = Claim.fromJson(req.query.data)
|
||||||
|
|
||||||
|
// Do verification
|
||||||
|
let data = await doVerification(profile)
|
||||||
|
|
||||||
|
data = data.toJSON()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Validate JSON
|
||||||
|
validate(data)
|
||||||
|
} catch (error) {
|
||||||
|
data = {
|
||||||
|
errors: [error.message]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let statusCode = 200
|
||||||
|
if ('errors' in data) {
|
||||||
|
statusCode = 500
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(statusCode).send(data)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default router
|
|
@ -51,9 +51,6 @@ const opts = {
|
||||||
telegram: {
|
telegram: {
|
||||||
token: process.env.TELEGRAM_TOKEN || null
|
token: process.env.TELEGRAM_TOKEN || null
|
||||||
},
|
},
|
||||||
twitter: {
|
|
||||||
bearerToken: process.env.TWITTER_BEARER_TOKEN || null
|
|
||||||
},
|
|
||||||
xmpp: {
|
xmpp: {
|
||||||
service: process.env.XMPP_SERVICE || null,
|
service: process.env.XMPP_SERVICE || null,
|
||||||
username: process.env.XMPP_USERNAME || null,
|
username: process.env.XMPP_USERNAME || null,
|
||||||
|
@ -138,26 +135,6 @@ router.get(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Twitter route
|
|
||||||
router.get('/twitter', query('tweetId').isInt(), async (req, res) => {
|
|
||||||
if (!opts.claims.twitter.bearerToken) {
|
|
||||||
return res.status(501).json({ errors: 'Twitter not enabled on server' })
|
|
||||||
}
|
|
||||||
const errors = validationResult(req)
|
|
||||||
if (!errors.isEmpty()) {
|
|
||||||
return res.status(400).json({ errors: errors.array() })
|
|
||||||
}
|
|
||||||
|
|
||||||
fetcher.twitter
|
|
||||||
.fn(req.query, opts)
|
|
||||||
.then((data) => {
|
|
||||||
return res.status(200).send(data)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
return res.status(400).json({ errors: err.message ? err.message : err })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Matrix route
|
// Matrix route
|
||||||
router.get(
|
router.get(
|
||||||
'/matrix',
|
'/matrix',
|
Loading…
Reference in a new issue