Skip to content

Commit

Permalink
refactor: replaces express with hono in provider-proxy (#815)
Browse files Browse the repository at this point in the history
  • Loading branch information
stalniy authored Feb 8, 2025
1 parent a832d52 commit f3e4991
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 88 deletions.
6 changes: 4 additions & 2 deletions apps/provider-proxy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@
"dependencies": {
"@akashnetwork/logging": "*",
"@akashnetwork/net": "*",
"@hono/node-server": "^1.13.8",
"@hono/zod-openapi": "^0.18.4",
"async-sema": "^3.1.1",
"bech32": "^2.0.0",
"cors": "^2.8.5",
"express": "^4.18.2",
"hono": "^4.6.20",
"lru-cache": "^11.0.2",
"uuid": "^9.0.0",
"ws": "^7.5.9"
Expand Down
65 changes: 32 additions & 33 deletions apps/provider-proxy/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import cors from "cors";
import express, { Express } from "express";
import { serve } from "@hono/node-server";
import { OpenAPIHono } from "@hono/zod-openapi";
import { Hono } from "hono";
import { cors } from "hono/cors";
import { RegExpRouter } from "hono/router/reg-exp-router";
import http from "http";
import { AddressInfo } from "net";

import { getAppStatus } from "./routes/getAppStatus";
import { proxyProviderRequest } from "./routes/proxyProviderRequest";
import { getAppStatus, statusRoute } from "./routes/getAppStatus";
import { proxyProviderRequest, proxyRoute } from "./routes/proxyProviderRequest";
import { WebsocketServer } from "./services/WebsocketServer";
import { container } from "./container";

export function createApp(): Express {
const app = express();
export function createApp(): Hono {
const app = new OpenAPIHono({ router: new RegExpRouter() });

const whitelist = [
const corsWhitelist = [
"http://localhost:3001",
"http://localhost:3000",
"https://cloudmos.grafana.net",
Expand All @@ -20,41 +24,36 @@ export function createApp(): Express {
"https://console-beta.akash.network"
];

app.disable("x-powered-by");
app.use(
"/*",
cors({
origin: function (origin, callback) {
if (!origin || whitelist.indexOf(origin) !== -1) {
callback(null, true);
} else {
container.httpLogger?.warn(`Cors refused: ${origin}`);
callback(new Error("Not allowed by CORS"));
}
}
allowMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH", "OPTIONS"],
origin: corsWhitelist,
maxAge: 60
})
);
app.use(express.json());
app.get("/status", getAppStatus);
app.post("/", proxyProviderRequest);
app.openapi(statusRoute, getAppStatus);
app.openapi(proxyRoute, proxyProviderRequest as any);

return app;
}

export async function startAppServer(port: number): Promise<AppServer> {
return new Promise(resolve => {
const app = createApp();
const httpAppServer = app.listen(port, () => {
resolve({
host: `http://localhost:${(httpAppServer.address() as AddressInfo).port}`,
close() {
wss.close();
httpAppServer.close();
}
});
});
const wss = new WebsocketServer(httpAppServer, container.certificateValidator, container.createWsLogger);
wss.listen();
});
const app = createApp();
const httpAppServer = serve({
fetch: app.fetch,
port
}) as http.Server;
const wss = new WebsocketServer(httpAppServer, container.certificateValidator, container.createWsLogger);
wss.listen();

return {
host: `http://localhost:${(httpAppServer.address() as AddressInfo).port}`,
close() {
wss.close();
httpAppServer.close();
}
};
}

export interface AppServer {
Expand Down
3 changes: 2 additions & 1 deletion apps/provider-proxy/src/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@ export const container = {
providerProxy,
certificateValidator,
httpLogger,
createWsLogger
createWsLogger,
netConfig
};
32 changes: 29 additions & 3 deletions apps/provider-proxy/src/routes/getAppStatus.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,36 @@
import { Request, Response } from "express";
import { z } from "@hono/zod-openapi";
import { createRoute } from "@hono/zod-openapi";
import { Context, TypedResponse } from "hono";

import packageJson from "../../package.json";
import { container } from "../container";
import { humanFileSize } from "../sizeUtils";

export async function getAppStatus(_: Request, res: Response): Promise<void> {
const AppStatus = z.object({
openClientWebSocketCount: z.number(),
totalRequestCount: z.number(),
totalTransferred: z.string(),
logStreaming: z.string(),
logDownload: z.string(),
eventStreaming: z.string(),
shell: z.string(),
version: z.string()
});

export const statusRoute = createRoute({
method: "get",
path: "/status",
responses: {
200: {
content: {
"application/json": { schema: AppStatus }
},
description: "Retrieve app status"
}
}
});

export async function getAppStatus(ctx: Context): Promise<TypedResponse<z.infer<typeof AppStatus>, 200>> {
const webSocketStats = container.wsStats.getItems();
const openClientWebSocketCount = webSocketStats.filter(x => !x.isClosed()).length;
const totalRequestCount = webSocketStats.reduce((a, b) => a + b.getStats().totalStats.count, 0);
Expand Down Expand Up @@ -35,7 +61,7 @@ export async function getAppStatus(_: Request, res: Response): Promise<void> {
data: 0
});

res.send({
return ctx.json({
openClientWebSocketCount,
totalRequestCount,
totalTransferred: humanFileSize(totalTransferred),
Expand Down
125 changes: 90 additions & 35 deletions apps/provider-proxy/src/routes/proxyProviderRequest.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,101 @@
import { NextFunction, Request as ExpressRequest, Response as ExpressResponse } from "express";
import { SupportedChainNetworks } from "@akashnetwork/net";
import { createRoute, z } from "@hono/zod-openapi";
import { bech32 } from "bech32";
import { Context, TypedResponse } from "hono";
import { ClientErrorStatusCode } from "hono/utils/http-status";
import { Readable } from "stream";

import { container } from "../container";
import { httpRetry } from "../utils/retry";

const DEFAULT_TIMEOUT = 5_000;
export async function proxyProviderRequest(req: ExpressRequest, incommingResponse: ExpressResponse, next: NextFunction): Promise<void> {
const { certPem, keyPem, method, body, url, network, providerAddress, timeout } = req.body;

try {
const proxyResult = await httpRetry(
() =>
container.providerProxy.connect(url, {
method,
body,
cert: certPem,
key: keyPem,
network,
providerAddress,
timeout: Number(timeout || DEFAULT_TIMEOUT) || DEFAULT_TIMEOUT
}),
{
retryIf: result => result.ok && (!result.response.statusCode || result.response.statusCode > 500)
}
);
const RequestPayload = z.object({
certPem: z.string().optional(),
keyPem: z.string().optional(),
method: z.enum(["GET", "POST", "PUT", "DELETE"]),
url: z.string().url(),
body: z.string().optional(),
network: z.enum(container.netConfig.getSupportedNetworks() as [SupportedChainNetworks]).describe("Blockchain network"),
providerAddress: z
.string()
.refine(v => !!bech32.decodeUnsafe(v), "is not bech32 address")
.describe("Bech32 representation of provider wallet address"),
timeout: z.number().optional()
});

if (proxyResult.ok === false && proxyResult.code === "insecureConnection") {
incommingResponse.status(400);
incommingResponse.send("Could not establish tls connection since server responded with non-tls response");
return;
export const proxyRoute = createRoute({
method: "post",
path: "/",
request: {
body: {
content: {
"application/json": {
schema: RequestPayload
}
}
}
},
responses: {
400: {
content: {
"text/plain": {
schema: z.string()
}
},
description: "Returned if it's not possible to establish secure connection with proxied host"
},
495: {
// https://http.dev/495
content: {
"text/plain": {
schema: z.string()
}
},
description: "Returned if host SSL certificate is invalid"
}
}
});

const DEFAULT_TIMEOUT = 5_000;
export async function proxyProviderRequest(ctx: Context): Promise<Response | TypedResponse<string>> {
const { certPem, keyPem, method, body, url, network, providerAddress, timeout } = await ctx.req.json<z.infer<typeof RequestPayload>>();

if (proxyResult.ok === false && proxyResult.code === "invalidCertificate") {
incommingResponse.status(495); // https://http.dev/495
incommingResponse.send(`Invalid certificate error: ${proxyResult.reason}`);
return;
const proxyResult = await httpRetry(
() =>
container.providerProxy.connect(url, {
method,
body,
cert: certPem,
key: keyPem,
network,
providerAddress,
timeout: Number(timeout || DEFAULT_TIMEOUT) || DEFAULT_TIMEOUT
}),
{
retryIf: result => result.ok && (!result.response.statusCode || result.response.statusCode > 500)
}
);

if (proxyResult.ok === false && proxyResult.code === "insecureConnection") {
return ctx.text("Could not establish tls connection since server responded with non-tls response", 400);
}

Object.keys(proxyResult.response.headers).forEach(header => {
incommingResponse.setHeader(header, proxyResult.response.headers[header] || "");
});
proxyResult.response.pipe(incommingResponse).on("error", next);
} catch (error) {
next(error);
if (proxyResult.ok === false && proxyResult.code === "invalidCertificate") {
return ctx.text(`Invalid certificate error: ${proxyResult.reason}`, 495 as ClientErrorStatusCode);
}

const headers = new Headers();
Object.keys(proxyResult.response.headers).forEach(header => {
const value = proxyResult.response.headers[header];
if (Array.isArray(value)) {
value.forEach(v => headers.set(header, v));
} else {
headers.set(header, value);
}
});

return new Response(Readable.toWeb(proxyResult.response), {
status: proxyResult.response.statusCode,
statusText: proxyResult.response.statusMessage,
headers
});
}
56 changes: 42 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions packages/net/src/NetConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ export class NetConfig {
getBaseAPIUrl(network: SupportedChainNetworks): string {
return netConfigData[network].apiUrls[0];
}

getSupportedNetworks(): SupportedChainNetworks[] {
return Object.keys(netConfigData) as SupportedChainNetworks[];
}
}

0 comments on commit f3e4991

Please sign in to comment.