mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 00:18:19 +01:00
feat(federation): ✨ Add bridge tokens, federation request debugging
This commit is contained in:
parent
673b7d0bae
commit
eab61b38f1
|
|
@ -294,9 +294,12 @@ avatars = []
|
|||
enabled = false
|
||||
# Only lysand-ap exists for now
|
||||
software = "lysand-ap"
|
||||
# WARNING: These IPs will have signature checks disabled.
|
||||
# Only use the bridge software if you trust it.
|
||||
# If this is empty, any bridge with the correct token
|
||||
# will be able to send data to your instance
|
||||
allowed_ips = ["192.168.1.0/24"]
|
||||
# Token for the bridge software
|
||||
# Bridge must have the same token!
|
||||
token = "mycooltoken"
|
||||
|
||||
[instance]
|
||||
name = "Lysand"
|
||||
|
|
|
|||
|
|
@ -527,6 +527,13 @@ export const configValidator = z.object({
|
|||
)
|
||||
.default({}),
|
||||
}),
|
||||
debug: z
|
||||
.object({
|
||||
federation: z.boolean().default(false),
|
||||
})
|
||||
.default({
|
||||
federation: false,
|
||||
}),
|
||||
});
|
||||
|
||||
export type Config = z.infer<typeof configValidator>;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { applyConfig, handleZodError } from "@api";
|
||||
import { applyConfig, debugRequest, handleZodError } from "@api";
|
||||
import { zValidator } from "@hono/zod-validator";
|
||||
import { dualLogger } from "@loggers";
|
||||
import {
|
||||
|
|
@ -43,6 +43,8 @@ export const schemas = {
|
|||
header: z.object({
|
||||
signature: z.string(),
|
||||
date: z.string(),
|
||||
authorization: z.string().optional(),
|
||||
origin: z.string(),
|
||||
}),
|
||||
body: z.any(),
|
||||
};
|
||||
|
|
@ -56,10 +58,34 @@ export default (app: Hono) =>
|
|||
zValidator("json", schemas.body, handleZodError),
|
||||
async (context) => {
|
||||
const { uuid } = context.req.valid("param");
|
||||
const { signature, date } = context.req.valid("header");
|
||||
const { signature, date, authorization, origin } =
|
||||
context.req.valid("header");
|
||||
|
||||
// Check if Origin is defederated
|
||||
if (
|
||||
config.federation.blocked.find(
|
||||
(blocked) =>
|
||||
blocked.includes(origin) || origin.includes(blocked),
|
||||
)
|
||||
) {
|
||||
// Pretend to accept request
|
||||
return response(null, 201);
|
||||
}
|
||||
|
||||
const body: typeof EntityValidator.$Entity =
|
||||
await context.req.valid("json");
|
||||
|
||||
if (config.debug.federation) {
|
||||
// Debug request
|
||||
await debugRequest(
|
||||
new Request(context.req.url, {
|
||||
method: context.req.method,
|
||||
headers: context.req.raw.headers,
|
||||
body: await context.req.text(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const user = await User.fromId(uuid);
|
||||
|
||||
if (!user) {
|
||||
|
|
@ -74,27 +100,38 @@ export default (app: Hono) =>
|
|||
|
||||
let checkSignature = true;
|
||||
|
||||
if (request_ip?.address && config.federation.bridge.enabled) {
|
||||
for (const ip of config.federation.bridge.allowed_ips) {
|
||||
if (matches(ip, request_ip?.address)) {
|
||||
checkSignature = false;
|
||||
break;
|
||||
if (config.federation.bridge.enabled) {
|
||||
const token = authorization?.split("Bearer ")[1];
|
||||
if (token) {
|
||||
// Request is bridge request
|
||||
if (token !== config.federation.bridge.token) {
|
||||
return errorResponse(
|
||||
"An invalid token was passed in the Authorization header. Please use the correct token, or remove the Authorization header.",
|
||||
401,
|
||||
);
|
||||
}
|
||||
|
||||
if (request_ip?.address) {
|
||||
if (config.federation.bridge.allowed_ips.length > 0)
|
||||
checkSignature = false;
|
||||
|
||||
for (const ip of config.federation.bridge.allowed_ips) {
|
||||
if (matches(ip, request_ip?.address)) {
|
||||
checkSignature = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return errorResponse(
|
||||
"Request IP address is not available",
|
||||
500,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify request signature
|
||||
// TODO: Check if instance is defederated
|
||||
// TODO: Reverse DNS lookup with Origin header
|
||||
if (checkSignature) {
|
||||
if (!signature) {
|
||||
return errorResponse("Missing Signature header", 400);
|
||||
}
|
||||
|
||||
if (!date) {
|
||||
return errorResponse("Missing Date header", 400);
|
||||
}
|
||||
|
||||
const keyId = signature
|
||||
.split("keyId=")[1]
|
||||
.split(",")[0]
|
||||
|
|
@ -177,6 +214,17 @@ export default (app: Hono) =>
|
|||
case "Follow": {
|
||||
const follow = await validator.Follow(body);
|
||||
|
||||
if (
|
||||
config.federation.discard.follows.find(
|
||||
(blocked) =>
|
||||
blocked.includes(origin) ||
|
||||
origin.includes(blocked),
|
||||
)
|
||||
) {
|
||||
// Pretend to accept request
|
||||
return response("Follow request sent", 200);
|
||||
}
|
||||
|
||||
const account = await User.resolve(follow.author);
|
||||
|
||||
if (!account) {
|
||||
|
|
@ -220,7 +268,16 @@ export default (app: Hono) =>
|
|||
case "FollowAccept": {
|
||||
const followAccept = await validator.FollowAccept(body);
|
||||
|
||||
console.log(followAccept);
|
||||
if (
|
||||
config.federation.discard.follows.find(
|
||||
(blocked) =>
|
||||
blocked.includes(origin) ||
|
||||
origin.includes(blocked),
|
||||
)
|
||||
) {
|
||||
// Pretend to accept request
|
||||
return response("Follow request accepted", 200);
|
||||
}
|
||||
|
||||
const account = await User.resolve(followAccept.author);
|
||||
|
||||
|
|
@ -255,6 +312,17 @@ export default (app: Hono) =>
|
|||
case "FollowReject": {
|
||||
const followReject = await validator.FollowReject(body);
|
||||
|
||||
if (
|
||||
config.federation.discard.follows.find(
|
||||
(blocked) =>
|
||||
blocked.includes(origin) ||
|
||||
origin.includes(blocked),
|
||||
)
|
||||
) {
|
||||
// Pretend to accept request
|
||||
return response("Follow request rejected", 200);
|
||||
}
|
||||
|
||||
const account = await User.resolve(followReject.author);
|
||||
|
||||
if (!account) {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ describe("API Tests", () => {
|
|||
expect(data.error).toContain("https://stackoverflow.com");
|
||||
});
|
||||
|
||||
test("try sending a request with a different origin", async () => {
|
||||
// Now automatically mitigated by the server
|
||||
/* test("try sending a request with a different origin", async () => {
|
||||
if (new URL(config.http.base_url).protocol === "http:") {
|
||||
return;
|
||||
}
|
||||
|
|
@ -59,5 +60,5 @@ describe("API Tests", () => {
|
|||
expect(response.status).toBe(400);
|
||||
const data = await response.json();
|
||||
expect(data.error).toContain("does not match base URL");
|
||||
});
|
||||
}); */
|
||||
});
|
||||
|
|
|
|||
21
utils/api.ts
21
utils/api.ts
|
|
@ -1,4 +1,6 @@
|
|||
import { consoleLogger } from "@loggers";
|
||||
import { errorResponse } from "@response";
|
||||
import chalk from "chalk";
|
||||
import { config } from "config-manager";
|
||||
import type { Context } from "hono";
|
||||
import { createMiddleware } from "hono/factory";
|
||||
|
|
@ -22,6 +24,7 @@ import { fromZodError } from "zod-validation-error";
|
|||
import type { Application } from "~database/entities/Application";
|
||||
import { getFromHeader } from "~database/entities/User";
|
||||
import type { User } from "~packages/database-interface/user";
|
||||
import { LogLevel } from "~packages/log-manager";
|
||||
import type { APIRouteMetadata, HttpVerb } from "~types/api";
|
||||
|
||||
export const applyConfig = (routeMeta: APIRouteMetadata) => {
|
||||
|
|
@ -298,3 +301,21 @@ export const jsonOrForm = () => {
|
|||
await next();
|
||||
});
|
||||
};
|
||||
|
||||
export const debugRequest = async (req: Request, logger = consoleLogger) => {
|
||||
const body = await req.clone().text();
|
||||
await logger.log(
|
||||
LogLevel.DEBUG,
|
||||
"RequestDebugger",
|
||||
`\n${chalk.green(req.method)} ${chalk.blue(req.url)}\n${chalk.bold(
|
||||
"Hash",
|
||||
)}: ${chalk.yellow(
|
||||
new Bun.SHA256().update(body).digest("hex"),
|
||||
)}\n${chalk.bold("Headers")}:\n${Array.from(req.headers.entries())
|
||||
.map(
|
||||
([key, value]) =>
|
||||
` - ${chalk.cyan(key)}: ${chalk.white(value)}`,
|
||||
)
|
||||
.join("\n")}\n${chalk.bold("Body")}: ${chalk.gray(body)}`,
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue