diff --git a/workspace/data-proxy/src/config-parser.ts b/workspace/data-proxy/src/config-parser.ts index f87b7df..5264d46 100644 --- a/workspace/data-proxy/src/config-parser.ts +++ b/workspace/data-proxy/src/config-parser.ts @@ -1,3 +1,4 @@ +import { maybe } from "@seda-protocol/utils/valibot"; import type { HTTPMethod } from "elysia"; import { Result } from "true-myth"; import * as v from "valibot"; @@ -7,6 +8,7 @@ import { replaceParams } from "./utils/replace-params"; const HttpMethodSchema = v.union([v.string(), v.array(v.string())]); const RouteSchema = v.object({ + publicEndpoint: maybe(v.string()), path: v.string(), upstreamUrl: v.string(), method: v.optional(HttpMethodSchema, DEFAULT_HTTP_METHODS), @@ -23,6 +25,7 @@ const RouteSchema = v.object({ const ConfigSchema = v.object({ routeGroup: v.optional(v.string(), DEFAULT_PROXY_ROUTE_GROUP), routes: v.array(RouteSchema), + publicEndpoint: maybe(v.string()), statusEndpoints: v.optional( v.object({ root: v.string(), diff --git a/workspace/data-proxy/src/proxy-server.test.ts b/workspace/data-proxy/src/proxy-server.test.ts index b3b620a..e15ba80 100644 --- a/workspace/data-proxy/src/proxy-server.test.ts +++ b/workspace/data-proxy/src/proxy-server.test.ts @@ -6,8 +6,9 @@ import { expect, it, } from "bun:test"; -import { Secp256k1 } from "@cosmjs/crypto"; +import { Secp256k1, Secp256k1Signature, keccak256 } from "@cosmjs/crypto"; import { DataProxy, Environment } from "@seda-protocol/data-proxy-sdk"; +import { Maybe } from "true-myth"; import { startProxyServer } from "./proxy-server"; import { HttpResponse, @@ -52,8 +53,10 @@ describe("proxy server", () => { statusEndpoints: { root: "status", }, + publicEndpoint: Maybe.nothing(), routes: [ { + publicEndpoint: Maybe.nothing(), method: "POST", path, upstreamUrl, @@ -98,8 +101,10 @@ describe("proxy server", () => { statusEndpoints: { root: "status", }, + publicEndpoint: Maybe.nothing(), routes: [ { + publicEndpoint: Maybe.nothing(), method: "GET", path, upstreamUrl, @@ -125,6 +130,128 @@ describe("proxy server", () => { await proxy.stop(); }); + describe("public endpoint configuration", () => { + it("should support rewriting the protocol and host at the root level", async () => { + const { upstreamUrl, proxyUrl, path, port } = registerHandler( + "get", + "/root-public-endpoint", + async () => { + return HttpResponse.json({ data: "hello" }); + }, + ); + + const proxy = startProxyServer( + { + routeGroup: "", + statusEndpoints: { + root: "status", + }, + publicEndpoint: Maybe.of("https://seda-data-proxy.com"), + routes: [ + { + publicEndpoint: Maybe.nothing(), + method: "GET", + path, + upstreamUrl, + forwardResponseHeaders: new Set([]), + headers: {}, + }, + ], + }, + dataProxy, + { + disableProof: true, + port, + }, + ); + + const response = await fetch(proxyUrl); + const result = await response.json(); + + const message = dataProxy.generateMessage( + // Fake a different public URL + `https://seda-data-proxy.com${path}`, + "GET", + Buffer.from(""), + Buffer.from(JSON.stringify(result)), + ); + + const signature = Secp256k1Signature.fromFixedLength( + Buffer.from(response.headers.get("x-seda-signature") ?? "", "hex"), + ); + const isValid = await Secp256k1.verifySignature( + signature, + keccak256(message), + Buffer.from(response.headers.get("x-seda-publickey") ?? "", "hex"), + ); + + expect(isValid, "Signature verification failed").toBe(true); + + await proxy.stop(); + }); + + it("should support rewriting the protocol and host at the route level", async () => { + const { upstreamUrl, proxyUrl, path, port } = registerHandler( + "get", + "/route-public-endpoint", + async () => { + return HttpResponse.json({ data: "hello" }); + }, + ); + + const proxy = startProxyServer( + { + routeGroup: "", + statusEndpoints: { + root: "status", + }, + publicEndpoint: Maybe.of("https://seda-data-proxy.com"), + routes: [ + { + publicEndpoint: Maybe.of( + "https://different-subdomain.seda-data-proxy.com", + ), + method: "GET", + path, + upstreamUrl, + forwardResponseHeaders: new Set([]), + headers: {}, + }, + ], + }, + dataProxy, + { + disableProof: true, + port, + }, + ); + + const response = await fetch(proxyUrl); + const result = await response.json(); + + const message = dataProxy.generateMessage( + // Fake a different public URL + `https://different-subdomain.seda-data-proxy.com${path}`, + "GET", + Buffer.from(""), + Buffer.from(JSON.stringify(result)), + ); + + const signature = Secp256k1Signature.fromFixedLength( + Buffer.from(response.headers.get("x-seda-signature") ?? "", "hex"), + ); + const isValid = await Secp256k1.verifySignature( + signature, + keccak256(message), + Buffer.from(response.headers.get("x-seda-publickey") ?? "", "hex"), + ); + + expect(isValid, "Signature verification failed").toBe(true); + + await proxy.stop(); + }); + }); + describe("status endpoints", () => { it("should return the status of the proxy for /health", async () => { const { upstreamUrl, proxyUrl, path, port } = registerHandler( @@ -148,8 +275,10 @@ describe("proxy server", () => { statusEndpoints: { root: "status", }, + publicEndpoint: Maybe.nothing(), routes: [ { + publicEndpoint: Maybe.nothing(), method: "GET", path, upstreamUrl, @@ -225,8 +354,10 @@ describe("proxy server", () => { statusEndpoints: { root: "status", }, + publicEndpoint: Maybe.nothing(), routes: [ { + publicEndpoint: Maybe.nothing(), method: "GET", path, upstreamUrl, @@ -273,8 +404,10 @@ describe("proxy server", () => { secret: "secret", }, }, + publicEndpoint: Maybe.nothing(), routes: [ { + publicEndpoint: Maybe.nothing(), method: "GET", path, upstreamUrl, diff --git a/workspace/data-proxy/src/proxy-server.ts b/workspace/data-proxy/src/proxy-server.ts index cee5939..8867b1b 100644 --- a/workspace/data-proxy/src/proxy-server.ts +++ b/workspace/data-proxy/src/proxy-server.ts @@ -87,7 +87,15 @@ export function startProxyServer( app.route( routeMethod, route.path, - async ({ headers, params, body, query, requestId, request }) => { + async ({ + headers, + params, + body, + path, + query, + requestId, + request, + }) => { const requestLogger = logger.child({ requestId }); // requestBody is now always a string because of the parse function in this route @@ -224,8 +232,23 @@ export function startProxyServer( logger.debug("Successfully applied request JSONpath"); } + // If the route or proxy has a public endpoint we replace the protocol and host with the public endpoint. + const calledEndpoint = route.publicEndpoint + .or(config.publicEndpoint) + .mapOr(request.url, (t) => { + const pathIndex = request.url.indexOf(path); + return `${t}${request.url.slice(pathIndex)}`; + }); + + logger.debug("Signing data", { + calledEndpoint, + method: request.method, + body: requestBody.unwrapOr(undefined), + responseData, + }); + const signature = await dataProxy.signData( - request.url, + calledEndpoint, request.method, Buffer.from(requestBody.isJust ? requestBody.value : "", "utf-8"), Buffer.from(responseData, "utf-8"),