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 6713325
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 9 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ The two required attributes are `path` and `upstreamUrl`. These specify how the
}
```

> [!IMPORTANT]
> The `OPTIONS` method is reserved and cannot be used for a route.
#### Base URL per route

In addition to specifying the `baseURL` at the root level you can also specify it per `route`. The `baseURL` at the `route` level will take precedence over one at the root level.
Expand All @@ -133,8 +136,8 @@ In addition to specifying the `baseURL` at the root level you can also specify i
"baseURL": "https://btc.data-proxy.com",
"path": "/btc-usd",
"upstreamUrl": "https://myapi.com/btc-usd"
},
],
}
]
}
```

Expand Down
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");
},
);
});
29 changes: 27 additions & 2 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 { 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 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: ${DEFAULT_PROXY_ROUTE_GROUP})`,
);
}

if (config.statusEndpoints.apiKey) {
const statusApiSecretEnvMatches =
Expand Down
1 change: 0 additions & 1 deletion workspace/data-proxy/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,5 @@ export const DEFAULT_HTTP_METHODS: HTTPMethod[] = [
"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
10 changes: 9 additions & 1 deletion workspace/data-proxy/src/proxy-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 6713325

Please sign in to comment.