mirror of
https://github.com/tale/headplane.git
synced 2026-07-26 07:48:14 +00:00
fix: handle registration keys on headscale 0.29+
Closes HP-555.
This commit is contained in:
@@ -12,10 +12,11 @@ import Text from "~/components/text";
|
||||
import Title from "~/components/title";
|
||||
import { useForm } from "~/hooks/use-form";
|
||||
import type { User } from "~/types";
|
||||
import { normalizeRegistrationKey } from "~/utils/register-key";
|
||||
import { getUserDisplayName } from "~/utils/user";
|
||||
|
||||
const registerSchema = type({
|
||||
register_key: "string == 24",
|
||||
register_key: "string > 0",
|
||||
user: "string > 0",
|
||||
});
|
||||
|
||||
@@ -28,7 +29,16 @@ export interface NewMachineProps {
|
||||
|
||||
export default function NewMachine(data: NewMachineProps) {
|
||||
const [pushDialog, setPushDialog] = useState(false);
|
||||
const form = useForm({ schema: registerSchema });
|
||||
const form = useForm({
|
||||
schema: registerSchema,
|
||||
validate: (values) =>
|
||||
normalizeRegistrationKey(String(values.register_key ?? ""))
|
||||
? undefined
|
||||
: {
|
||||
register_key:
|
||||
"Paste the registration URL or full hskey-authreq-... key from tailscale up.",
|
||||
},
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
@@ -43,7 +53,8 @@ export default function NewMachine(data: NewMachineProps) {
|
||||
{...form.field("register_key")}
|
||||
required
|
||||
label="Machine Key"
|
||||
placeholder="AbCd..."
|
||||
placeholder="hskey-authreq-XXXXXXXXXXXXXXXXXXXXXXXX"
|
||||
description="Paste the registration URL or full key shown by tailscale up."
|
||||
/>
|
||||
<Select
|
||||
required
|
||||
|
||||
@@ -4,6 +4,7 @@ import { authContext, headscaleLiveStoreContext, requestApiContext } from "~/ser
|
||||
import { isDataWithApiError } from "~/server/headscale/api/error-client";
|
||||
import { nodesResource } from "~/server/headscale/live-store";
|
||||
import { Capabilities } from "~/server/web/roles";
|
||||
import { normalizeRegistrationKey } from "~/utils/register-key";
|
||||
|
||||
import type { Route } from "./+types/machine";
|
||||
|
||||
@@ -31,13 +32,20 @@ export async function machineAction({ request, context }: Route.ActionArgs) {
|
||||
});
|
||||
}
|
||||
|
||||
const registrationKey = formData.get("register_key")?.toString();
|
||||
if (!registrationKey) {
|
||||
const registrationKeyInput = formData.get("register_key")?.toString();
|
||||
if (!registrationKeyInput) {
|
||||
throw data("Missing `register_key` in the form data.", {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const registrationKey = normalizeRegistrationKey(registrationKeyInput);
|
||||
if (!registrationKey) {
|
||||
throw data("Invalid `register_key` in the form data.", {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const user = formData.get("user")?.toString();
|
||||
if (!user) {
|
||||
throw data("Missing `user` in the form data.", {
|
||||
|
||||
@@ -36,6 +36,14 @@ export interface Capabilities {
|
||||
* 0.28.0+.
|
||||
*/
|
||||
readonly nodeOwnerIsImmutable: boolean;
|
||||
|
||||
/**
|
||||
* The `POST /api/v1/node/register` endpoint expects the full
|
||||
* `hskey-authreq-<id>` AuthID as the `key` parameter. Pre-0.29
|
||||
* Headscale expected the raw 24-character registration ID without
|
||||
* the `hskey-authreq-` prefix. Introduced in 0.29.0.
|
||||
*/
|
||||
readonly registerKeyIncludesAuthReqPrefix: boolean;
|
||||
}
|
||||
|
||||
export function capabilitiesFor(version: ServerVersion): Capabilities {
|
||||
@@ -43,5 +51,6 @@ export function capabilitiesFor(version: ServerVersion): Capabilities {
|
||||
preAuthKeysHaveStableIds: gte(version, "0.28.0"),
|
||||
nodeTagsAreFlat: gte(version, "0.28.0"),
|
||||
nodeOwnerIsImmutable: gte(version, "0.28.0"),
|
||||
registerKeyIncludesAuthReqPrefix: gte(version, "0.29.0"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -62,14 +62,19 @@ export function makeNodeApi(
|
||||
register: async (user, key) => {
|
||||
// Headscale's node-register endpoint expects the registration
|
||||
// params as both query string and body — preserved as-is.
|
||||
// Pre-0.29 expects the raw 24-char registration ID; 0.29+ expects
|
||||
// the full `hskey-authreq-<id>` AuthID.
|
||||
const registerKey = capabilities.registerKeyIncludesAuthReqPrefix
|
||||
? key
|
||||
: key.replace(/^hskey-authreq-/, "");
|
||||
const qp = new URLSearchParams();
|
||||
qp.append("user", user);
|
||||
qp.append("key", key);
|
||||
qp.append("key", registerKey);
|
||||
const { node } = await transport.request<{ node: RawMachine }>({
|
||||
method: "POST",
|
||||
path: `v1/node/register?${qp.toString()}`,
|
||||
apiKey,
|
||||
body: { user, key },
|
||||
body: { user, key: registerKey },
|
||||
});
|
||||
return normalize(node);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
const authRequestKeyPattern = /^hskey-authreq-[A-Za-z0-9_-]{16,}$/;
|
||||
const authRequestKeySearchPattern = /hskey-authreq-[A-Za-z0-9_-]{16,}/;
|
||||
const legacyMachineKeyPattern = /^mkey:[A-Za-z0-9_-]{16,}$/;
|
||||
const registerUrlPattern = /\/register\/([^\s?#]+)/;
|
||||
const registrationKeySuffixPattern = /^[A-Za-z0-9_-]{24,}$/;
|
||||
|
||||
export function normalizeRegistrationKey(value: string): string | null {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const registerUrlMatch = trimmed.match(registerUrlPattern);
|
||||
if (registerUrlMatch) {
|
||||
const token = normalizeRegistrationToken(registerUrlMatch[1]);
|
||||
if (token) {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
const authRequestKeyMatch = trimmed.match(authRequestKeySearchPattern);
|
||||
if (authRequestKeyMatch) {
|
||||
return authRequestKeyMatch[0];
|
||||
}
|
||||
|
||||
return normalizeRegistrationToken(trimmed);
|
||||
}
|
||||
|
||||
function normalizeRegistrationToken(value: string): string | null {
|
||||
let token: string;
|
||||
try {
|
||||
token = decodeURIComponent(value.trim());
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (authRequestKeyPattern.test(token) || legacyMachineKeyPattern.test(token)) {
|
||||
return token;
|
||||
}
|
||||
|
||||
if (registrationKeySuffixPattern.test(token)) {
|
||||
return `hskey-authreq-${token}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user