Skip to content

Commit

Permalink
feat(data-proxy): add config for specifying a public endpoint
Browse files Browse the repository at this point in the history
Closes: #32
  • Loading branch information
Thomasvdam committed Nov 4, 2024
1 parent ca53e26 commit 6fdaa86
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 3 deletions.
3 changes: 3 additions & 0 deletions workspace/data-proxy/src/config-parser.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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),
Expand All @@ -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(),
Expand Down
135 changes: 134 additions & 1 deletion workspace/data-proxy/src/proxy-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -52,8 +53,10 @@ describe("proxy server", () => {
statusEndpoints: {
root: "status",
},
publicEndpoint: Maybe.nothing(),
routes: [
{
publicEndpoint: Maybe.nothing(),
method: "POST",
path,
upstreamUrl,
Expand Down Expand Up @@ -98,8 +101,10 @@ describe("proxy server", () => {
statusEndpoints: {
root: "status",
},
publicEndpoint: Maybe.nothing(),
routes: [
{
publicEndpoint: Maybe.nothing(),
method: "GET",
path,
upstreamUrl,
Expand All @@ -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 <statusRoot>/health", async () => {
const { upstreamUrl, proxyUrl, path, port } = registerHandler(
Expand All @@ -148,8 +275,10 @@ describe("proxy server", () => {
statusEndpoints: {
root: "status",
},
publicEndpoint: Maybe.nothing(),
routes: [
{
publicEndpoint: Maybe.nothing(),
method: "GET",
path,
upstreamUrl,
Expand Down Expand Up @@ -225,8 +354,10 @@ describe("proxy server", () => {
statusEndpoints: {
root: "status",
},
publicEndpoint: Maybe.nothing(),
routes: [
{
publicEndpoint: Maybe.nothing(),
method: "GET",
path,
upstreamUrl,
Expand Down Expand Up @@ -273,8 +404,10 @@ describe("proxy server", () => {
secret: "secret",
},
},
publicEndpoint: Maybe.nothing(),
routes: [
{
publicEndpoint: Maybe.nothing(),
method: "GET",
path,
upstreamUrl,
Expand Down
27 changes: 25 additions & 2 deletions workspace/data-proxy/src/proxy-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"),
Expand Down

0 comments on commit 6fdaa86

Please sign in to comment.