kopia lustrzana https://github.com/cloudflare/wildebeest
Merge pull request #255 from Distal-Labs/fix-missing-apps-verify_credentials-endpoint
Fix missing /api/v1/apps/verify_credentials endpointsven/neon
commit
7060d27a93
|
@ -1 +1 @@
|
|||
16.13
|
||||
16.13.2
|
||||
|
|
|
@ -34,6 +34,14 @@ export function clientUnknown(): Response {
|
|||
return generateErrorResponse(`The client is unknown or invalid`, 403)
|
||||
}
|
||||
|
||||
export function methodNotAllowed(): Response {
|
||||
return generateErrorResponse(`Method not allowed`, 405)
|
||||
}
|
||||
|
||||
export function unprocessableEntity(detail: string): Response {
|
||||
return generateErrorResponse(`Unprocessable entity`, 422, detail)
|
||||
}
|
||||
|
||||
export function internalServerError(): Response {
|
||||
return generateErrorResponse('Internal Server Error', 500)
|
||||
}
|
||||
|
|
|
@ -2,11 +2,10 @@ import { strict as assert } from 'node:assert/strict'
|
|||
import type { Env } from 'wildebeest/backend/src/types/env'
|
||||
import * as v1_instance from 'wildebeest/functions/api/v1/instance'
|
||||
import * as v2_instance from 'wildebeest/functions/api/v2/instance'
|
||||
import * as apps from 'wildebeest/functions/api/v1/apps'
|
||||
import * as custom_emojis from 'wildebeest/functions/api/v1/custom_emojis'
|
||||
import * as mutes from 'wildebeest/functions/api/v1/mutes'
|
||||
import * as blocks from 'wildebeest/functions/api/v1/blocks'
|
||||
import { makeDB, assertCORS, assertJSON, assertCache, generateVAPIDKeys } from './utils'
|
||||
import { makeDB, assertCORS, assertJSON, assertCache } from './utils'
|
||||
import { enrichStatus } from 'wildebeest/backend/src/mastodon/microformats'
|
||||
import { moveFollowers } from 'wildebeest/backend/src/mastodon/follow'
|
||||
import { createPerson } from 'wildebeest/backend/src/activitypub/actors'
|
||||
|
@ -89,52 +88,6 @@ describe('Mastodon APIs', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('apps', () => {
|
||||
test('return the app infos', async () => {
|
||||
const db = await makeDB()
|
||||
const vapidKeys = await generateVAPIDKeys()
|
||||
const request = new Request('https://example.com', {
|
||||
method: 'POST',
|
||||
body: '{"redirect_uris":"mastodon://joinmastodon.org/oauth","website":"https://app.joinmastodon.org/ios","client_name":"Mastodon for iOS","scopes":"read write follow push"}',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
const res = await apps.handleRequest(db, request, vapidKeys)
|
||||
assert.equal(res.status, 200)
|
||||
assertCORS(res)
|
||||
assertJSON(res)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { name, website, redirect_uri, client_id, client_secret, vapid_key, id, ...rest } = await res.json<
|
||||
Record<string, string>
|
||||
>()
|
||||
|
||||
assert.equal(name, 'Mastodon for iOS')
|
||||
assert.equal(website, 'https://app.joinmastodon.org/ios')
|
||||
assert.equal(redirect_uri, 'mastodon://joinmastodon.org/oauth')
|
||||
assert.equal(id, '20')
|
||||
assert.deepEqual(rest, {})
|
||||
})
|
||||
|
||||
test('returns 404 for GET request', async () => {
|
||||
const vapidKeys = await generateVAPIDKeys()
|
||||
const request = new Request('https://example.com')
|
||||
const ctx: any = {
|
||||
next: () => new Response(),
|
||||
data: null,
|
||||
env: {
|
||||
VAPID_JWK: JSON.stringify(vapidKeys),
|
||||
},
|
||||
request,
|
||||
}
|
||||
|
||||
const res = await apps.onRequest(ctx)
|
||||
assert.equal(res.status, 400)
|
||||
})
|
||||
})
|
||||
|
||||
describe('custom emojis', () => {
|
||||
test('returns an empty array', async () => {
|
||||
const res = await custom_emojis.onRequest()
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
import { makeDB, assertCORS, assertJSON, createTestClient, generateVAPIDKeys } from '../utils'
|
||||
import { TEST_JWT } from '../test-data'
|
||||
import { strict as assert } from 'node:assert/strict'
|
||||
import * as apps from 'wildebeest/functions/api/v1/apps'
|
||||
import * as verify_app from 'wildebeest/functions/api/v1/apps/verify_credentials'
|
||||
import { CredentialApp } from 'wildebeest/functions/api/v1/apps/verify_credentials'
|
||||
import { VAPIDPublicKey } from 'wildebeest/backend/src/mastodon/subscription'
|
||||
|
||||
describe('Mastodon APIs', () => {
|
||||
describe('/apps', () => {
|
||||
test('POST /apps registers client', async () => {
|
||||
const db = await makeDB()
|
||||
const vapidKeys = await generateVAPIDKeys()
|
||||
const request = new Request('https://example.com', {
|
||||
method: 'POST',
|
||||
body: '{"redirect_uris":"mastodon://joinmastodon.org/oauth","website":"https://app.joinmastodon.org/ios","client_name":"Mastodon for iOS","scopes":"read write follow push"}',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
const res = await apps.handleRequest(db, request, vapidKeys)
|
||||
assert.equal(res.status, 200)
|
||||
assertCORS(res)
|
||||
assertJSON(res)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { name, website, redirect_uri, client_id, client_secret, vapid_key, id, ...rest } = await res.json<
|
||||
Record<string, string>
|
||||
>()
|
||||
|
||||
assert.equal(name, 'Mastodon for iOS')
|
||||
assert.equal(website, 'https://app.joinmastodon.org/ios')
|
||||
assert.equal(redirect_uri, 'mastodon://joinmastodon.org/oauth')
|
||||
assert.equal(id, '20')
|
||||
assert.deepEqual(rest, {})
|
||||
})
|
||||
|
||||
test('POST /apps returns 422 for malformed requests', async () => {
|
||||
// client_name and redirect_uris are required according to https://docs.joinmastodon.org/methods/apps/#form-data-parameters
|
||||
const db = await makeDB()
|
||||
const vapidKeys = await generateVAPIDKeys()
|
||||
const headers = { 'content-type': 'application/json' }
|
||||
|
||||
const validURIException = new Request('https://example.com', {
|
||||
method: 'POST',
|
||||
body: '{"redirect_uris":"urn:ietf:wg:oauth:2.0:oob","client_name":"Mastodon for iOS"}',
|
||||
headers: headers,
|
||||
})
|
||||
let res = await apps.handleRequest(db, validURIException, vapidKeys)
|
||||
assert.equal(res.status, 200)
|
||||
|
||||
const invalidURIRequest = new Request('https://example.com', {
|
||||
method: 'POST',
|
||||
body: '{"redirect_uris":"joinmastodon.org/oauth","client_name":"Mastodon for iOS"}',
|
||||
headers: headers,
|
||||
})
|
||||
res = await apps.handleRequest(db, invalidURIRequest, vapidKeys)
|
||||
assert.equal(res.status, 422)
|
||||
|
||||
const missingURIRequest = new Request('https://example.com', {
|
||||
method: 'POST',
|
||||
body: '{"client_name":"Mastodon for iOS"}',
|
||||
headers: headers,
|
||||
})
|
||||
res = await apps.handleRequest(db, missingURIRequest, vapidKeys)
|
||||
assert.equal(res.status, 422)
|
||||
|
||||
const missingClientNameRequest = new Request('https://example.com', {
|
||||
method: 'POST',
|
||||
body: '{"redirect_uris":"joinmastodon.org/oauth"}',
|
||||
headers: headers,
|
||||
})
|
||||
res = await apps.handleRequest(db, missingClientNameRequest, vapidKeys)
|
||||
assert.equal(res.status, 422)
|
||||
})
|
||||
|
||||
test('GET /apps is bad request', async () => {
|
||||
const vapidKeys = await generateVAPIDKeys()
|
||||
const request = new Request('https://example.com')
|
||||
const ctx: any = {
|
||||
next: () => new Response(),
|
||||
data: null,
|
||||
env: {
|
||||
VAPID_JWK: JSON.stringify(vapidKeys),
|
||||
},
|
||||
request,
|
||||
}
|
||||
|
||||
const res = await apps.onRequest(ctx)
|
||||
assert.equal(res.status, 405)
|
||||
})
|
||||
|
||||
test('GET /verify_credentials returns public VAPID key for known clients', async () => {
|
||||
const db = await makeDB()
|
||||
const testScope = 'test abcd'
|
||||
const client = await createTestClient(db, 'https://localhost', testScope)
|
||||
const vapidKeys = await generateVAPIDKeys()
|
||||
|
||||
const headers = { authorization: 'Bearer ' + client.id + '.' + TEST_JWT }
|
||||
|
||||
const req = new Request('https://example.com/api/v1/verify_credentials', { headers })
|
||||
|
||||
const res = await verify_app.handleRequest(db, req, vapidKeys)
|
||||
assert.equal(res.status, 200)
|
||||
assertCORS(res)
|
||||
assertJSON(res)
|
||||
|
||||
const jsonResponse: CredentialApp = await res.json()
|
||||
const publicVAPIDKey = VAPIDPublicKey(vapidKeys)
|
||||
assert.equal(jsonResponse.name, 'test client')
|
||||
assert.equal(jsonResponse.website, 'https://cloudflare.com')
|
||||
assert.equal(jsonResponse.vapid_key, publicVAPIDKey)
|
||||
})
|
||||
|
||||
test('GET /verify_credentials returns 403 for unauthorized clients', async () => {
|
||||
const db = await makeDB()
|
||||
const vapidKeys = await generateVAPIDKeys()
|
||||
|
||||
const headers = { authorization: 'Bearer APPID.' + TEST_JWT }
|
||||
|
||||
const req = new Request('https://example.com/api/v1/verify_credentials', { headers })
|
||||
|
||||
const res = await verify_app.handleRequest(db, req, vapidKeys)
|
||||
assert.equal(res.status, 403)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,5 +1,6 @@
|
|||
import { ContextData } from 'wildebeest/backend/src/types/context'
|
||||
import { cors } from 'wildebeest/backend/src/utils/cors'
|
||||
import * as errors from 'wildebeest/backend/src/errors'
|
||||
import type { JWK } from 'wildebeest/backend/src/webpush/jwk'
|
||||
import type { Env } from 'wildebeest/backend/src/types/env'
|
||||
import { createClient } from 'wildebeest/backend/src/mastodon/client'
|
||||
|
@ -21,10 +22,27 @@ export const onRequest: PagesFunction<Env, any, ContextData> = async ({ request,
|
|||
|
||||
export async function handleRequest(db: Database, request: Request, vapidKeys: JWK) {
|
||||
if (request.method !== 'POST') {
|
||||
return new Response('', { status: 400 })
|
||||
return errors.methodNotAllowed()
|
||||
}
|
||||
|
||||
const body = await readBody<AppsPost>(request)
|
||||
const body: AppsPost = await readBody<AppsPost>(request)
|
||||
|
||||
// Parameter validation according to https://github.com/mastodon/mastodon/blob/main/app/lib/application_extension.rb
|
||||
if (body.client_name === undefined || body.client_name?.trim() === '') {
|
||||
return errors.unprocessableEntity('client_name cannot be an empty string')
|
||||
} else if (body.client_name?.length > 60) {
|
||||
return errors.unprocessableEntity('client_name cannot exceed 60 characters')
|
||||
} else if (body.redirect_uris === undefined || body.redirect_uris?.trim() === '') {
|
||||
return errors.unprocessableEntity('redirect_uris cannot be an empty string')
|
||||
} else if (body.redirect_uris?.length > 2000) {
|
||||
return errors.unprocessableEntity('redirect_uris cannot exceed 2000 characters')
|
||||
} else if (body.redirect_uris !== 'urn:ietf:wg:oauth:2.0:oob') {
|
||||
try {
|
||||
new URL('', body.redirect_uris)
|
||||
} catch {
|
||||
return errors.unprocessableEntity('redirect_uris must be a valid URI')
|
||||
}
|
||||
}
|
||||
|
||||
const client = await createClient(db, body.client_name, body.redirect_uris, body.website, body.scopes)
|
||||
const vapidKey = VAPIDPublicKey(vapidKeys)
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
// https://docs.joinmastodon.org/methods/apps/#verify_credentials
|
||||
|
||||
import { cors } from 'wildebeest/backend/src/utils/cors'
|
||||
import { VAPIDPublicKey } from 'wildebeest/backend/src/mastodon/subscription'
|
||||
import { getVAPIDKeys } from 'wildebeest/backend/src/config'
|
||||
import { getClientById } from 'wildebeest/backend/src/mastodon/client'
|
||||
import type { Env } from 'wildebeest/backend/src/types/env'
|
||||
import type { JWK } from 'wildebeest/backend/src/webpush/jwk'
|
||||
import type { ContextData } from 'wildebeest/backend/src/types/context'
|
||||
import * as errors from 'wildebeest/backend/src/errors'
|
||||
|
||||
export type CredentialApp = {
|
||||
name: string
|
||||
website: string
|
||||
vapid_key: string
|
||||
}
|
||||
|
||||
const headers = {
|
||||
...cors(),
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
}
|
||||
|
||||
export const onRequest: PagesFunction<Env, any, ContextData> = async ({ request, env }) => {
|
||||
return handleRequest(env.DATABASE, request, getVAPIDKeys(env))
|
||||
}
|
||||
|
||||
export async function handleRequest(db: D1Database, request: Request, vapidKeys: JWK) {
|
||||
if (request.method !== 'GET') {
|
||||
return new Response('', { status: 400 })
|
||||
}
|
||||
|
||||
const authHeader = request.headers.get('Authorization')?.replace('Bearer ', '')
|
||||
const parts = authHeader?.split('.') ?? ''
|
||||
const clientId = parts[0]
|
||||
|
||||
const client = await getClientById(db, clientId)
|
||||
if (client === null) {
|
||||
return errors.clientUnknown()
|
||||
}
|
||||
const vapidKey = VAPIDPublicKey(vapidKeys)
|
||||
|
||||
const res = {
|
||||
name: client.name,
|
||||
website: client.website,
|
||||
vapid_key: vapidKey,
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(res), { headers })
|
||||
}
|
Ładowanie…
Reference in New Issue