mirror of https://github.com/openclaw/openclaw.git
Matrix: use authenticated media downloads
This commit is contained in:
parent
9ab8df25d3
commit
6bc46f16b8
|
|
@ -216,6 +216,49 @@ describe("MatrixClient request hardening", () => {
|
|||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("prefers authenticated client media downloads", async () => {
|
||||
const payload = Buffer.from([1, 2, 3, 4]);
|
||||
const fetchMock = vi.fn(async () => new Response(payload, { status: 200 }));
|
||||
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
|
||||
|
||||
const client = new MatrixClient("https://matrix.example.org", "token");
|
||||
await expect(client.downloadContent("mxc://example.org/media")).resolves.toEqual(payload);
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
const firstUrl = String(fetchMock.mock.calls[0]?.[0]);
|
||||
expect(firstUrl).toContain("/_matrix/client/v1/media/download/example.org/media");
|
||||
});
|
||||
|
||||
it("falls back to legacy media downloads for older homeservers", async () => {
|
||||
const payload = Buffer.from([5, 6, 7, 8]);
|
||||
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (url.includes("/_matrix/client/v1/media/download/")) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
errcode: "M_UNRECOGNIZED",
|
||||
error: "Unrecognized request",
|
||||
}),
|
||||
{
|
||||
status: 404,
|
||||
headers: { "content-type": "application/json" },
|
||||
},
|
||||
);
|
||||
}
|
||||
return new Response(payload, { status: 200 });
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
|
||||
|
||||
const client = new MatrixClient("https://matrix.example.org", "token");
|
||||
await expect(client.downloadContent("mxc://example.org/media")).resolves.toEqual(payload);
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
const firstUrl = String(fetchMock.mock.calls[0]?.[0]);
|
||||
const secondUrl = String(fetchMock.mock.calls[1]?.[0]);
|
||||
expect(firstUrl).toContain("/_matrix/client/v1/media/download/example.org/media");
|
||||
expect(secondUrl).toContain("/_matrix/media/v3/download/example.org/media");
|
||||
});
|
||||
|
||||
it("decrypts encrypted room events returned by getEvent", async () => {
|
||||
const client = new MatrixClient("https://matrix.example.org", "token");
|
||||
matrixJsClient.fetchRoomEvent = vi.fn(async () => ({
|
||||
|
|
|
|||
|
|
@ -152,6 +152,20 @@ function isMatrixNotFoundError(err: unknown): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
function isUnsupportedAuthenticatedMediaEndpointError(err: unknown): boolean {
|
||||
const statusCode = (err as { statusCode?: number })?.statusCode;
|
||||
if (statusCode === 404 || statusCode === 405 || statusCode === 501) {
|
||||
return true;
|
||||
}
|
||||
const message = (err instanceof Error ? err.message : String(err)).toLowerCase();
|
||||
return (
|
||||
message.includes("m_unrecognized") ||
|
||||
message.includes("unrecognized request") ||
|
||||
message.includes("method not allowed") ||
|
||||
message.includes("not implemented")
|
||||
);
|
||||
}
|
||||
|
||||
export class MatrixClient {
|
||||
private readonly client: MatrixJsClient;
|
||||
private readonly emitter = new EventEmitter();
|
||||
|
|
@ -625,16 +639,29 @@ export class MatrixClient {
|
|||
if (!parsed) {
|
||||
throw new Error(`Invalid Matrix content URI: ${mxcUrl}`);
|
||||
}
|
||||
const endpoint = `/_matrix/media/v3/download/${encodeURIComponent(parsed.server)}/${encodeURIComponent(parsed.mediaId)}`;
|
||||
const response = await this.httpClient.requestRaw({
|
||||
method: "GET",
|
||||
endpoint,
|
||||
qs: { allow_remote: opts.allowRemote ?? true },
|
||||
timeoutMs: this.localTimeoutMs,
|
||||
maxBytes: opts.maxBytes,
|
||||
readIdleTimeoutMs: opts.readIdleTimeoutMs,
|
||||
});
|
||||
return response;
|
||||
const encodedServer = encodeURIComponent(parsed.server);
|
||||
const encodedMediaId = encodeURIComponent(parsed.mediaId);
|
||||
const request = async (endpoint: string): Promise<Buffer> =>
|
||||
await this.httpClient.requestRaw({
|
||||
method: "GET",
|
||||
endpoint,
|
||||
qs: { allow_remote: opts.allowRemote ?? true },
|
||||
timeoutMs: this.localTimeoutMs,
|
||||
maxBytes: opts.maxBytes,
|
||||
readIdleTimeoutMs: opts.readIdleTimeoutMs,
|
||||
});
|
||||
|
||||
const authenticatedEndpoint = `/_matrix/client/v1/media/download/${encodedServer}/${encodedMediaId}`;
|
||||
try {
|
||||
return await request(authenticatedEndpoint);
|
||||
} catch (err) {
|
||||
if (!isUnsupportedAuthenticatedMediaEndpointError(err)) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const legacyEndpoint = `/_matrix/media/v3/download/${encodedServer}/${encodedMediaId}`;
|
||||
return await request(legacyEndpoint);
|
||||
}
|
||||
|
||||
async uploadContent(file: Buffer, contentType?: string, filename?: string): Promise<string> {
|
||||
|
|
|
|||
Loading…
Reference in New Issue