Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cors #56

Merged
merged 3 commits into from
Oct 31, 2023
Merged

Cors #56

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import { config } from "./src/config.js";
import GET from "./src/fetch/GET.js";
import OPTIONS from "./src/fetch/OPTIONS.js";
import POST from "./src/fetch/POST.js";
import PUT from "./src/fetch/PUT.js";
import { NotFound } from "./src/fetch/cors.js";
import { logger } from "./src/logger.js";

if (config.verbose) logger.enable();
Expand All @@ -12,10 +14,11 @@ const app = Bun.serve({
hostname: config.hostname,
port: config.port,
fetch(req: Request) {
if (req.method == "GET") return GET(req);
if (req.method == "POST") return POST(req);
if (req.method == "PUT") return PUT(req);
return new Response("Invalid request", { status: 400 });
if (req.method === "GET") return GET(req);
if (req.method === "POST") return POST(req);
if (req.method === "PUT") return PUT(req);
if (req.method === "OPTIONS") return OPTIONS(req);
return NotFound;
},
});

Expand Down
15 changes: 8 additions & 7 deletions src/fetch/GET.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { file } from "bun";
import swaggerFavicon from "../../swagger/favicon.png";
import swaggerHtml from "../../swagger/index.html";
import { registry } from "../prometheus.js";
import { metrics } from "../prometheus.js";
import { blocks } from "./blocks.js";
import { NotFound, toFile, toJSON } from "./cors.js";
import health from "./health.js";
import openapi from "./openapi.js";

export default async function (req: Request) {
const { pathname } = new URL(req.url);

if (pathname === "/") return new Response(file(swaggerHtml));
if (pathname === "/favicon.png") return new Response(file(swaggerFavicon));
if (pathname === "/health") return health(req);
if (pathname === "/metrics") return new Response(await registry.metrics(), { headers: { "Content-Type": registry.contentType } });
if (pathname === "/openapi") return new Response(openapi, { headers: { "Content-Type": "application/json" } });
if (pathname === "/") return toFile(file(swaggerHtml));
if (pathname === "/favicon.png") return toFile(file(swaggerFavicon));
if (pathname === "/health") return health();
if (pathname === "/metrics") return metrics();
if (pathname === "/openapi") return toJSON(openapi);
if (pathname === "/blocks") return blocks();

return new Response("Not found", { status: 400 });
return NotFound;
}
5 changes: 5 additions & 0 deletions src/fetch/OPTIONS.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { CORS_HEADERS } from "./cors.js";

export default async function (req: Request) {
return new Response("Departed", { headers: CORS_HEADERS });
}
9 changes: 6 additions & 3 deletions src/fetch/POST.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { handleSinkRequest } from "../clickhouse/handleSinkRequest.js";
import { logger } from "../logger.js";
import { sink_request_errors, sink_requests } from "../prometheus.js";
import { BodySchema } from "../schemas.js";
import signatureEd25519 from "../webhook/signatureEd25519.js";
import { toText } from "./cors.js";
import { query } from "./query.js";

export default async function (req: Request) {
Expand All @@ -22,13 +24,14 @@ export default async function (req: Request) {
const body = BodySchema.parse(JSON.parse(text));

if ("message" in body) {
if (body.message === "PING") return new Response("OK");
return new Response("invalid body", { status: 400 });
if (body.message === "PING") return toText("OK");
return toText("invalid body", 400);
}

return handleSinkRequest(body);
} catch (err) {
logger.error(err);
sink_request_errors?.inc();
return new Response("invalid request: " + JSON.stringify(err), { status: 400 });
return toText("invalid request", 400);
}
}
3 changes: 2 additions & 1 deletion src/fetch/PUT.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { validateBearerAuth } from "./bearerAuth.js";
import { NotFound } from "./cors.js";
import init from "./init.js";
import { handleSchemaRequest } from "./schema.js";

Expand All @@ -13,5 +14,5 @@ export default async function (req: Request): Promise<Response> {
if (pathname === "/schema/sql") return handleSchemaRequest(req, "sql");
if (pathname === "/schema/graphql") return handleSchemaRequest(req, "graphql");

return new Response("Not found", { status: 400 });
return NotFound;
}
7 changes: 5 additions & 2 deletions src/fetch/blocks.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { z } from "zod";
import client from "../clickhouse/createClient.js";
import { logger } from "../logger.js";
import { InternalServerError, toJSON } from "./cors.js";

type BlockViewType = Array<{
count: string;
Expand Down Expand Up @@ -41,8 +43,9 @@ export async function blocks(): Promise<Response> {

const dto: BlockResponseSchema = { max, min, distinctCount, delta, missing, count };

return new Response(JSON.stringify(dto), { headers: { "Content-Type": "application/json" } });
return toJSON(dto);
} catch (err) {
return new Response(err instanceof Error ? err.message : JSON.stringify(err), { status: 500 });
logger.error(err);
return InternalServerError;
}
}
37 changes: 37 additions & 0 deletions src/fetch/cors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { BunFile } from "bun";

export const CORS_HEADERS = new Headers({
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, WWW-Authenticate",
});
export const JSON_HEADERS = new Headers({ "Content-Type": "application/json" });
export const TEXT_HEADERS = new Headers({ "Content-Type": "text/plain" });

export const BadRequest = toText("Bad Request", 400);
export const NotFound = toText("Not Found", 404);
export const InternalServerError = toText("Internal Server Error", 500);

export function appendHeaders(...args: Headers[]) {
const headers = new Headers(CORS_HEADERS); // CORS as default headers
for (const arg of args) {
for (const [key, value] of arg.entries()) {
headers.set(key, value);
}
}
return headers;
}

export function toJSON(body: unknown, status = 200, headers = new Headers()) {
const data = typeof body == "string" ? body : JSON.stringify(body);
return new Response(data, { status, headers: appendHeaders(JSON_HEADERS, headers) });
}

export function toText(body: string, status = 200, headers = new Headers()) {
return new Response(body, { status, headers: appendHeaders(TEXT_HEADERS, headers) });
}

export function toFile(body: BunFile, status = 200, headers = new Headers()) {
const fileHeaders = new Headers({ "Content-Type": body.type });
return new Response(body, { status, headers: appendHeaders(fileHeaders, headers) });
}
16 changes: 11 additions & 5 deletions src/fetch/health.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import client from "../clickhouse/createClient.js";
import { logger } from "../logger.js";
import { BadRequest, toText } from "./cors.js";

export default async function (req: Request) {
export default async function () {
try {
const response = await client.ping();
if (response.success === false) throw new Error(response.error.message);
return new Response("OK");
} catch (e: any) {
return new Response(e.message, { status: 400 });
if (!response.success) {
throw new Error(response.error.message);
}

return toText("OK");
} catch (e) {
logger.error(e);
return BadRequest;
}
}
7 changes: 5 additions & 2 deletions src/fetch/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ import { createDatabase } from "../clickhouse/createDatabase.js";
import { ping } from "../clickhouse/ping.js";
import { initializeDefaultTables } from "../clickhouse/table-initialization.js";
import { config } from "../config.js";
import { logger } from "../logger.js";
import { BadRequest, toText } from "./cors.js";

export default async function () {
try {
await ping();
await createDatabase(config.database);
await initializeDefaultTables();
return new Response("OK");
return toText("OK");
} catch (e) {
return new Response(e instanceof Error ? e.message : JSON.stringify(e), { status: 400 });
logger.error(e);
return BadRequest;
}
}
10 changes: 5 additions & 5 deletions src/fetch/query.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { readOnlyClient } from "../clickhouse/createClient.js";
import { logger } from "../logger.js";
import { BadRequest, toJSON } from "./cors.js";

export async function query(req: Request): Promise<Response> {
try {
const query = await req.text();
const result = await readOnlyClient.query({ query, format: "JSONEachRow" });
const data = await result.json();

return new Response(JSON.stringify(data), {
status: 200,
headers: { "Content-Type": "application/json" },
});
return toJSON(data);
} catch (err) {
return new Response("Bad request: " + err, { status: 400 });
logger.error(err);
return BadRequest;
}
}
12 changes: 6 additions & 6 deletions src/fetch/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@ import { ClickhouseTableBuilder } from "../graphql/builders/clickhouse-table-bui
import { TableTranslator } from "../graphql/table-translator.js";
import { logger } from "../logger.js";
import { TableInitSchema } from "../schemas.js";
import { toJSON, toText } from "./cors.js";

const clickhouseBuilder = new ClickhouseTableBuilder();

export async function handleSchemaRequest(req: Request, type: "sql" | "graphql") {
const body = await req.text();
if (!body) {
return new Response("missing body", { status: 400 });
return toText("missing body", 400);
}

const result = TableInitSchema.safeParse(body);
if (!result.success) {
return new Response(result.error.toString(), { status: 400 });
return toText("Bad request: " + result.error.toString(), 400);
}

let tableSchemas: string[] = [];
Expand All @@ -29,10 +30,9 @@ export async function handleSchemaRequest(req: Request, type: "sql" | "graphql")

try {
await initializeTables(tableSchemas);
return new Response(JSON.stringify({ status: "OK", schema: tableSchemas.join("\n\n") }), {
headers: { "Content-Type": "application/json" },
});
return toJSON({ status: "OK", schema: tableSchemas.join("\n\n") });
} catch (err) {
return new Response(`Could not create the tables: ${err}`, { status: 500 });
logger.error(err);
return toText("Could not create the tables", 500);
}
}
6 changes: 6 additions & 0 deletions src/prometheus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ import client, { Counter, Gauge } from "prom-client";

export const registry = new client.Registry();

export async function metrics() {
const headers = new Headers();
headers.set("Content-Type", registry.contentType);
return new Response(await registry.metrics(), { status: 200, headers });
}

export function registerCounter(name: string, help: string) {
try {
registry.registerMetric(new client.Counter({ name, help }));
Expand Down
17 changes: 9 additions & 8 deletions src/webhook/signatureEd25519.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { config } from "../config.js";
import { toText } from "../fetch/cors.js";
import { verify } from "./verify.js";

export default async function (req: Request, text: string) {
const timestamp = req.headers.get("x-signature-timestamp");
const signature = req.headers.get("x-signature-ed25519");
const timestamp = req.headers.get("x-signature-timestamp");
const signature = req.headers.get("x-signature-ed25519");

if (!timestamp) return new Response("missing required timestamp in headers", {status: 400});
if (!signature) return new Response("missing required signature in headers", {status: 400 });
if (!text) return new Response("missing body", { status: 400 });
if (!timestamp) return toText("missing required timestamp in headers", 400);
if (!signature) return toText("missing required signature in headers", 400);
if (!text) return toText("missing body", 400);

const msg = Buffer.from(timestamp + text);
const isVerified = verify(msg, signature, config.publicKey);
const msg = Buffer.from(timestamp + text);
const isVerified = verify(msg, signature, config.publicKey);

if (!isVerified) return new Response("invalid request signature", { status: 401 });
if (!isVerified) return toText("invalid request signature", 401);
}