From 4fb0a185f63bce0e5742cc7f9816fc1b7aa1509e Mon Sep 17 00:00:00 2001 From: DecDuck Date: Tue, 13 May 2025 12:28:18 +1000 Subject: [PATCH] feat: preliminary peer api --- package.json | 2 +- server/api/v1/client/peer/index.get.ts | 20 +++++++++ server/api/v1/client/peer/index.post.ts | 26 ++++++++++++ server/internal/p2p/headscale.ts | 55 ++++++++++++++++++++++++- yarn.lock | 31 ++++++++++++-- 5 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 server/api/v1/client/peer/index.get.ts create mode 100644 server/api/v1/client/peer/index.post.ts diff --git a/package.json b/package.json index ca073bb9..699d1d27 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ }, "dependencies": { "@drop-oss/droplet": "^0.7.2", - "@drop-oss/headscalez": "^0.0.3", + "@drop-oss/headscalez": "0.0.4", "@headlessui/vue": "^1.7.23", "@heroicons/vue": "^2.1.5", "@nuxt/fonts": "^0.11.0", diff --git a/server/api/v1/client/peer/index.get.ts b/server/api/v1/client/peer/index.get.ts new file mode 100644 index 00000000..c37d8cdf --- /dev/null +++ b/server/api/v1/client/peer/index.get.ts @@ -0,0 +1,20 @@ +import { ClientCapabilities } from "~/prisma/client"; +import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; +import headscaleManager from "~/server/internal/p2p/headscale"; + +export default defineClientEventHandler(async (h3, { fetchClient }) => { + const client = await fetchClient(); + if (!client.capabilities.includes(ClientCapabilities.PeerAPI)) + throw createError({ + statusCode: 403, + statusMessage: "Capability not allowed.", + }); + + if (!headscaleManager.enabled()) + throw createError({ + statusCode: 500, + statusMessage: "Peer network not available.", + }); + + return headscaleManager.configuration(); +}); diff --git a/server/api/v1/client/peer/index.post.ts b/server/api/v1/client/peer/index.post.ts new file mode 100644 index 00000000..775a7f44 --- /dev/null +++ b/server/api/v1/client/peer/index.post.ts @@ -0,0 +1,26 @@ +import { ClientCapabilities } from "~/prisma/client"; +import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; +import headscaleManager from "~/server/internal/p2p/headscale"; + +export default defineClientEventHandler( + async (h3, { fetchClient, fetchUser }) => { + const client = await fetchClient(); + if (!client.capabilities.includes(ClientCapabilities.PeerAPI)) + throw createError({ + statusCode: 403, + statusMessage: "Capability not allowed.", + }); + + if (!headscaleManager.enabled()) + throw createError({ + statusCode: 500, + statusMessage: "Peer network not available.", + }); + + const user = await fetchUser(); + + const key = await headscaleManager.createPreauthKey(user, client); + + return key; + }, +); diff --git a/server/internal/p2p/headscale.ts b/server/internal/p2p/headscale.ts index 0aacff1f..24723348 100644 --- a/server/internal/p2p/headscale.ts +++ b/server/internal/p2p/headscale.ts @@ -1,9 +1,12 @@ import type { HeadscaleService } from "@drop-oss/headscalez"; -import { startHeadscale } from "@drop-oss/headscalez"; +import { HeadscaleControlService, startHeadscale } from "@drop-oss/headscalez"; import { systemConfig } from "../config/sys-conf"; +import type { Client, User } from "~/prisma/client"; export class HeadscaleManager { + private controlURL?: string; private headscaleService?: HeadscaleService; + private headscaleClient?: HeadscaleControlService; constructor() { this.setup(); @@ -12,17 +15,67 @@ export class HeadscaleManager { async setup() { const externalUrl = process.env.CONTROL_URL; if (externalUrl) { + this.controlURL = externalUrl; const headscale = await startHeadscale({ externalUrl, dir: systemConfig.getHeadscaleFolder(), }); this.headscaleService = headscale; + this.headscaleClient = new HeadscaleControlService(headscale); } } enabled() { return !!this.headscaleService; } + + configuration() { + if (!this.controlURL) throw new Error("Headscale not available"); + return { + controlURL: this.controlURL, + }; + } + + private async fetchUser(user: User) { + if (!this.headscaleClient) + throw new Error("Headscale client not available"); + const { response } = await this.headscaleClient.client.listUsers({ + id: 0n, + name: user.id, + email: "", + }); + + if (response.users.length == 0) { + const { response } = await this.headscaleClient.client.createUser({ + name: user.id, + displayName: user.displayName, + email: user.email, + pictureUrl: user.profilePictureObjectId, + }); + + if (!response.user) throw new Error("Could not create user"); + + return response.user; + } + + return response.users[0]; + } + + async createPreauthKey(user: User, client: Client) { + if (!this.headscaleClient) + throw new Error("Headscale client not available"); + + const headscaleUser = await this.fetchUser(user); + const { response } = await this.headscaleClient.client.createPreAuthKey({ + user: headscaleUser.name, + reusable: false, + ephemeral: false, + aclTags: ["client", `user:${user.id}`, `client:${client.id}`], + }); + + if (!response.preAuthKey) throw new Error("Could not create pre-auth key"); + return response.preAuthKey; + } } export const headscaleManager = new HeadscaleManager(); diff --git a/yarn.lock b/yarn.lock index 92c2a1b4..68fcd2c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -371,11 +371,14 @@ "@drop-oss/droplet-win32-arm64-msvc" "0.7.2" "@drop-oss/droplet-win32-x64-msvc" "0.7.2" -"@drop-oss/headscalez@^0.0.3": - version "0.0.3" - resolved "https://registry.yarnpkg.com/@drop-oss/headscalez/-/headscalez-0.0.3.tgz#deb6eae13b9b65cf225bb2e0e84bc47159b4a91e" - integrity sha512-Zsl0T/pwJyw7vbLwEUVAf9zXKKTG3qAbUVQEDiRe88bwhzauPG9/MgBBHA2N/9GHXMUFRRDcchPbwGjCcmcJ3Q== +"@drop-oss/headscalez@0.0.4": + version "0.0.4" + resolved "https://registry.yarnpkg.com/@drop-oss/headscalez/-/headscalez-0.0.4.tgz#7558fbbc542d93fb0c69bdcf48917bdae676c62d" + integrity sha512-vdDq6Qe/T5S0FKKYdnBXvOh53wGs8nxy6O3ACVyUjTbTGkDn3z5og4joh5TxoAOFodPHRLCihVuroiymS7yNTg== dependencies: + "@protobuf-ts/grpcweb-transport" "^2.10.0" + "@protobuf-ts/runtime" "^2.10.0" + "@protobuf-ts/runtime-rpc" "^2.10.0" execa "^9.5.3" fs-extra "^11.3.0" node-graceful-shutdown "^1.1.5" @@ -1549,6 +1552,26 @@ dependencies: "@prisma/debug" "6.7.0" +"@protobuf-ts/grpcweb-transport@^2.10.0": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@protobuf-ts/grpcweb-transport/-/grpcweb-transport-2.10.0.tgz#c09f29694ec7dc4fc1107a0bf30d359325939311" + integrity sha512-VUyD+8kn4XEfWoEiKjAsWX5XWkt8Gfdp/uquS17yt9hdfMBVx3o5tlXX5ylwfWhcIjbRvkf7WHEEdS2wMuq86Q== + dependencies: + "@protobuf-ts/runtime" "^2.10.0" + "@protobuf-ts/runtime-rpc" "^2.10.0" + +"@protobuf-ts/runtime-rpc@^2.10.0": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@protobuf-ts/runtime-rpc/-/runtime-rpc-2.10.0.tgz#68de8dcc369e56579569a4deafd394cf4683dc66" + integrity sha512-8CS/XPv3+pMK4v8UKhtCdvbS4h9l7aqlteKdRt0/UbIKZ8n0qHj6hX8cBhz2ngvohxCOS0N08zPr9aCLBNhW3Q== + dependencies: + "@protobuf-ts/runtime" "^2.10.0" + +"@protobuf-ts/runtime@^2.10.0": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@protobuf-ts/runtime/-/runtime-2.10.0.tgz#bc90f632647ff2ae72887546ddf3d193f3f43d98" + integrity sha512-ypYwGg9Pn3W/2lZ7/HW60hONGuSdzphvOY8Dq7LeNttymDe0y3LaTUUMRpuGqOT6FfrWEMnfQbyqU8AAreo8wA== + "@rollup/plugin-alias@^5.1.1": version "5.1.1" resolved "https://registry.yarnpkg.com/@rollup/plugin-alias/-/plugin-alias-5.1.1.tgz#53601d88cda8b1577aa130b4a6e452283605bf26"