Skip to content

Commit

Permalink
feat(data-proxy)!: add handshake method
Browse files Browse the repository at this point in the history
The OPTIONS http method is no longer allowed, as we need this to be able
to reliably retrieve the data proxy info from the overlay network.

Closes: #34
  • Loading branch information
Thomasvdam committed Nov 27, 2024
1 parent 47f1518 commit 5f2b6b4
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 12 deletions.
2 changes: 1 addition & 1 deletion workspace/data-proxy-sdk/src/data-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export interface SignedData {
}

export class DataProxy {
private version = "0.1.0";
public version = "0.1.0";
public publicKey: Buffer;
private privateKey: Buffer;
public options: DataProxyOptions;
Expand Down
35 changes: 34 additions & 1 deletion workspace/data-proxy/src/config-parser.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { describe, expect, it } from "bun:test";
import { assertIsOkResult } from "@seda-protocol/utils/testing";
import {
assertIsErrorResult,
assertIsOkResult,
} from "@seda-protocol/utils/testing";
import { parseConfig } from "./config-parser";

describe("parseConfig", () => {
Expand Down Expand Up @@ -120,4 +123,34 @@ describe("parseConfig", () => {

expect(result).toBeOkResult();
});

it("should fail if the status endpoint uses a the same route group as the proxy", () => {
const result = parseConfig({
statusEndpoints: {
root: "proxy",
},
routes: [],
});

assertIsErrorResult(result);
expect(result.error).toContain("can not be the same");
});

it.each(["OPTIONS", ["OPTIONS", "GET"]])(
"should error when the OPTIONS method is used for a route",
(method) => {
const resultSingle = parseConfig({
routes: [
{
method,
path: "/:coinA/*",
upstreamUrl: "aaaaaa.com/{*}",
},
],
});

assertIsErrorResult(resultSingle);
expect(resultSingle.error).toContain("OPTIONS method is reserved");
},
);
});
33 changes: 29 additions & 4 deletions workspace/data-proxy/src/config-parser.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { tryParseSync } from "@seda-protocol/utils";
import { maybe } from "@seda-protocol/utils/valibot";
import type { HTTPMethod } from "elysia";
import { Result } from "true-myth";
import * as v from "valibot";
import { DEFAULT_HTTP_METHODS, DEFAULT_PROXY_ROUTE_GROUP } from "./constants";
import { DEFAULT_HTTP_METHODS, PROXY_ROUTE_GROUP } from "./constants";
import { replaceParams } from "./utils/replace-params";

const HttpMethodSchema = v.union([v.string(), v.array(v.string())]);
const NotOptionsMethod = v.pipe(
v.string(),
v.notValue("OPTIONS", "OPTIONS method is reserved"),
);
const HttpMethodSchema = v.union([NotOptionsMethod, v.array(NotOptionsMethod)]);

const RouteSchema = v.object({
baseURL: maybe(v.string()),
Expand All @@ -23,7 +28,7 @@ const RouteSchema = v.object({
});

const ConfigSchema = v.object({
routeGroup: v.optional(v.string(), DEFAULT_PROXY_ROUTE_GROUP),
routeGroup: v.optional(v.string(), PROXY_ROUTE_GROUP),
routes: v.array(RouteSchema),
baseURL: maybe(v.string()),
statusEndpoints: v.optional(
Expand Down Expand Up @@ -58,7 +63,27 @@ const pathRegex = new RegExp(/{(:[^}]+)}/g, "g");
const envVariablesRegex = new RegExp(/{(\$[^}]+)}/g, "g");

export function parseConfig(input: unknown): Result<Config, string> {
const config = v.parse(ConfigSchema, input);
const configResult = tryParseSync(ConfigSchema, input);
if (configResult.isErr) {
return Result.err(
configResult.error
.map((err) => {
const key = err.path?.reduce((path, segment) => {
return path.concat(".", segment.key as string);
}, "");
return `${key}: ${err.message}`;
})
.join("\n"),
);
}

const config = configResult.value;

if (config.statusEndpoints.root === config.routeGroup) {
return Result.err(
`"statusEndpoints.root" can not be the same as "routeGroup" (value: ${PROXY_ROUTE_GROUP})`,
);
}

if (config.statusEndpoints.apiKey) {
const statusApiSecretEnvMatches =
Expand Down
4 changes: 2 additions & 2 deletions workspace/data-proxy/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ export const DEFAULT_PRIVATE_KEY_JSON_FILE_NAME =
"./data-proxy-private-key.json";

// Where all the proxy routes go to (For example /proxy/CONFIGURED_ROUTE_HERE)
export const DEFAULT_PROXY_ROUTE_GROUP = "proxy";
export const PROXY_ROUTE_GROUP = "proxy";
export const INFO_ROUTE_GROUP = "info";
// Default http methods set when no method is provided in the config
export const DEFAULT_HTTP_METHODS: HTTPMethod[] = [
"GET",
"PATCH",
"POST",
"PUT",
"DELETE",
"OPTIONS",
"HEAD",
];
54 changes: 53 additions & 1 deletion workspace/data-proxy/src/proxy-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import {
it,
} from "bun:test";
import { Secp256k1, Secp256k1Signature, keccak256 } from "@cosmjs/crypto";
import { DataProxy, Environment } from "@seda-protocol/data-proxy-sdk";
import {
constants,
DataProxy,
Environment,
} from "@seda-protocol/data-proxy-sdk";
import { Maybe } from "true-myth";
import { startProxyServer } from "./proxy-server";
import {
Expand Down Expand Up @@ -252,6 +256,54 @@ describe("proxy server", () => {
});
});

describe("OPTIONS methods", () => {
it("should return the public key and version of the data proxy", async () => {
const { upstreamUrl, proxyUrl, path, port } = registerHandler(
"get",
"/test",
async () => {
return HttpResponse.json({ data: "info" });
},
);

const proxy = startProxyServer(
{
routeGroup: "",
statusEndpoints: {
root: "status",
},
baseURL: Maybe.nothing(),
routes: [
{
baseURL: Maybe.nothing(),
method: "GET",
path,
upstreamUrl,
forwardResponseHeaders: new Set([]),
headers: {},
jsonPath: "$.data",
},
],
},
dataProxy,
{
disableProof: true,
port,
},
);

const response = await fetch(proxyUrl, { method: "OPTIONS" });
const version = response.headers.get(
constants.SIGNATURE_VERSION_HEADER_KEY,
);
const publicKey = response.headers.get(constants.PUBLIC_KEY_HEADER_KEY);

expect(version).toBe("0.1.0");
expect(publicKey).toBe(
"031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f",
);
});
});
describe("status endpoints", () => {
it("should return the status of the proxy for <statusRoot>/health", async () => {
const { upstreamUrl, proxyUrl, path, port } = registerHandler(
Expand Down
14 changes: 11 additions & 3 deletions workspace/data-proxy/src/proxy-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { tryAsync } from "@seda-protocol/utils";
import { Elysia } from "elysia";
import { Maybe } from "true-myth";
import { type Config, getHttpMethods } from "./config-parser";
import { DEFAULT_PROXY_ROUTE_GROUP, JSON_PATH_HEADER_KEY } from "./constants";
import { JSON_PATH_HEADER_KEY, PROXY_ROUTE_GROUP } from "./constants";
import logger from "./logger";
import { StatusContext, statusPlugin } from "./status-plugin";
import {
Expand Down Expand Up @@ -68,7 +68,7 @@ export function startProxyServer(
const statusContext = new StatusContext(dataProxy.publicKey.toString("hex"));
server.use(statusPlugin(statusContext, config.statusEndpoints));

const proxyGroup = config.routeGroup ?? DEFAULT_PROXY_ROUTE_GROUP;
const proxyGroup = config.routeGroup ?? PROXY_ROUTE_GROUP;

server.group(proxyGroup, (app) => {
// Only update the status context in routes that are part of the proxy group
Expand All @@ -80,9 +80,17 @@ export function startProxyServer(
});

for (const route of config.routes) {
const routeMethods = getHttpMethods(route.method);
app.route("OPTIONS", route.path, () => {
const headers = new Headers({
[constants.PUBLIC_KEY_HEADER_KEY]:
dataProxy.publicKey.toString("hex"),
[constants.SIGNATURE_VERSION_HEADER_KEY]: dataProxy.version,
});
return new Response(null, { headers });
});

// A route can have multiple methods attach to it
const routeMethods = getHttpMethods(route.method);
for (const routeMethod of routeMethods) {
app.route(
routeMethod,
Expand Down

0 comments on commit 5f2b6b4

Please sign in to comment.