atproto/packages/pds/tests/proxied/proxy-header.test.ts
Matthieu Sieben 72eba67af1
Drop axios dependency (#3177)
* Minor adaptation of VerifyCidTransform implementation

* refactor: factorize content-encoding negotiation into new lib

* bsky: Use undici to stream blob

* fixup! bsky: Use undici to stream blob

* disable ssrf bsky protection in dev-env

* remove http requests to self to host "/img/"

* drop axios from tests

* fixes

* fix tests

* reviex changes

* properly handle HEAD requests

* handle client disconnection

* fix tests

* drop unrelated change

* tidy

* tidy

* tidy

* remove axios from dev-env

* remove axios from identity package

* use undici 6

* remove axios dependency from ozone

* tidy

* remove axios from PDS package

* avoid killing bsky-pds connections

* improve debugging data

* Better handle invalid CID

* tidy

* tidy

* refactor "allFulfilled" util in @atproto/common

* tidy

---------

Co-authored-by: devin ivy <devinivy@gmail.com>
2025-01-06 18:34:11 +01:00

203 lines
5.4 KiB
TypeScript

import { Keypair } from '@atproto/crypto'
import { SeedClient, TestNetworkNoAppView, usersSeed } from '@atproto/dev-env'
import { verifyJwt } from '@atproto/xrpc-server'
import * as plc from '@did-plc/lib'
import express from 'express'
import assert from 'node:assert'
import { once } from 'node:events'
import http from 'node:http'
import { AddressInfo } from 'node:net'
import { parseProxyHeader } from '../../src/pipethrough'
describe('proxy header', () => {
let network: TestNetworkNoAppView
let sc: SeedClient
let alice: string
let proxyServer: ProxyServer
beforeAll(async () => {
network = await TestNetworkNoAppView.create({
dbPostgresSchema: 'proxy_header',
})
sc = network.getSeedClient()
await usersSeed(sc)
proxyServer = await ProxyServer.create(
network.pds.ctx.plcClient,
network.pds.ctx.plcRotationKey,
'atproto_test',
)
alice = sc.dids.alice
await network.processAll()
})
afterAll(async () => {
await proxyServer.close()
await network.close()
})
it('parses proxy header', async () => {
expect(parseProxyHeader(network.pds.ctx, `#atproto_test`)).rejects.toThrow(
'no did specified in proxy header',
)
expect(
parseProxyHeader(network.pds.ctx, `${proxyServer.did}#atproto_test#foo`),
).rejects.toThrow('invalid proxy header format')
expect(
parseProxyHeader(network.pds.ctx, `${proxyServer.did}#atproto_test `),
).rejects.toThrow('proxy header cannot contain spaces')
expect(
parseProxyHeader(network.pds.ctx, ` ${proxyServer.did}#atproto_test`),
).rejects.toThrow('proxy header cannot contain spaces')
expect(parseProxyHeader(network.pds.ctx, `foo#bar`)).rejects.toThrow(
'Poorly formatted DID: foo',
)
expect(
parseProxyHeader(network.pds.ctx, `${proxyServer.did}#atproto_test`),
).resolves.toEqual({
did: proxyServer.did,
url: proxyServer.url,
})
})
it('proxies requests based on header', async () => {
const path = `/xrpc/app.bsky.actor.getProfile?actor=${alice}`
await fetch(`${network.pds.url}${path}`, {
headers: {
...sc.getHeaders(alice),
'atproto-proxy': `${proxyServer.did}#atproto_test`,
},
})
const req = proxyServer.requests.at(-1)
assert(req)
expect(req.url).toEqual(path)
assert(req.auth)
const verified = await verifyJwt(
req.auth.replace('Bearer ', ''),
proxyServer.did,
'app.bsky.actor.getProfile',
(iss) => network.pds.ctx.idResolver.did.resolveAtprotoKey(iss, true),
)
expect(verified.aud).toBe(proxyServer.did)
expect(verified.iss).toBe(alice)
})
it('fails on a non-existant did', async () => {
const path = `/xrpc/app.bsky.actor.getProfile?actor=${alice}`
const response = await fetch(`${network.pds.url}${path}`, {
headers: {
...sc.getHeaders(alice),
'atproto-proxy': `did:plc:12345678123456781234578#atproto_test`,
},
})
await expect(response.json()).resolves.toMatchObject({
message: 'could not resolve proxy did',
})
expect(proxyServer.requests.length).toBe(1)
})
it('fails when a service is not specified', async () => {
const path = `/xrpc/app.bsky.actor.getProfile?actor=${alice}`
const response = await fetch(`${network.pds.url}${path}`, {
headers: {
...sc.getHeaders(alice),
'atproto-proxy': proxyServer.did,
},
})
await expect(response.json()).resolves.toMatchObject({
message: 'no service id specified in proxy header',
})
expect(proxyServer.requests.length).toBe(1)
})
it('fails on a non-existant service', async () => {
const path = `/xrpc/app.bsky.actor.getProfile?actor=${alice}`
const response = await fetch(`${network.pds.url}${path}`, {
headers: {
...sc.getHeaders(alice),
'atproto-proxy': `${proxyServer.did}#atproto_bad`,
},
})
await expect(response.json()).resolves.toMatchObject({
message: 'could not resolve proxy did service url',
})
expect(proxyServer.requests.length).toBe(1)
})
})
type ProxyReq = {
url: string
auth: string | undefined
}
class ProxyServer {
constructor(
public server: http.Server,
public url: string,
public did: string,
public requests: ProxyReq[],
) {}
static async create(
plcClient: plc.Client,
keypair: Keypair,
serviceId: string,
): Promise<ProxyServer> {
const requests: ProxyReq[] = []
const app = express()
app.get('*', (req, res) => {
requests.push({
url: req.url,
auth: req.header('authorization'),
})
res.sendStatus(200)
})
const server = app.listen(0)
await once(server, 'listening')
const { port } = server.address() as AddressInfo
const url = `http://localhost:${port}`
const plcOp = await plc.signOperation(
{
type: 'plc_operation',
rotationKeys: [keypair.did()],
alsoKnownAs: [],
verificationMethods: {},
services: {
[serviceId]: {
type: 'TestAtprotoService',
endpoint: url,
},
},
prev: null,
},
keypair,
)
const did = await plc.didForCreateOp(plcOp)
await plcClient.sendOperation(did, plcOp)
return new ProxyServer(server, url, did, requests)
}
close(): Promise<void> {
return new Promise<void>((resolve) => {
this.server.close(() => resolve())
})
}
}