import { timingSafeEqual } from "node:crypto"; import type { LfsBatchRequest, LfsObjectResult, LfsVerifyRequest } from "./lfs"; import { lfsJson, lfsError } from "./lfs"; import { presignR2Url } from "./s3-presign"; const OID_RE = /^[0-9a-f]{64}$/; const ROUTE_RE = /^\/([^/]+?)(?:\.git)?\/info\/lfs\/objects\/(batch|verify)$/; function authenticate(request: Request, env: Env): boolean { const auth = request.headers.get("Authorization"); if (!auth?.startsWith("Basic ")) return false; let decoded: string; try { decoded = atob(auth.slice(6)); } catch { return false; } const colon = decoded.indexOf(":"); if (colon === -1) return false; const user = decoded.slice(0, colon); const pass = decoded.slice(colon + 1); const enc = new TextEncoder(); const expectedUser = enc.encode(env.R2_ACCESS_KEY_ID); const expectedPass = enc.encode(env.R2_SECRET_ACCESS_KEY); const givenUser = enc.encode(user); const givenPass = enc.encode(pass); if (expectedUser.byteLength !== givenUser.byteLength || expectedPass.byteLength !== givenPass.byteLength) { return false; } return timingSafeEqual(givenUser, expectedUser) && timingSafeEqual(givenPass, expectedPass); } async function handleBatch(request: Request, env: Env, repo: string): Promise { const body = (await request.json()) as LfsBatchRequest; const { operation, objects } = body; if (operation !== "download" && operation !== "upload") { return lfsError(422, `Unsupported operation: ${operation}`); } if (!objects?.length) { return lfsError(422, "No objects in request"); } const results = await Promise.all( objects.map(async (obj): Promise => { if (!OID_RE.test(obj.oid)) { return { oid: obj.oid, size: obj.size, error: { code: 422, message: "Invalid OID" } }; } const key = `${repo}/${obj.oid}`; if (operation === "download") { const head = await env.LFS_BUCKET.head(key); if (!head) { return { oid: obj.oid, size: obj.size, error: { code: 404, message: "Object not found" } }; } const href = await presignR2Url({ accountId: env.R2_ACCOUNT_ID, bucket: env.R2_BUCKET_NAME, key, accessKeyId: env.R2_ACCESS_KEY_ID, secretAccessKey: env.R2_SECRET_ACCESS_KEY, method: "GET", }); return { oid: obj.oid, size: obj.size, authenticated: true, actions: { download: { href, expires_in: 3600 } }, }; } // upload const head = await env.LFS_BUCKET.head(key); if (head && head.size === obj.size && head.customMetadata?.sha256 === obj.oid) { // Already uploaded, skip return { oid: obj.oid, size: obj.size }; } const href = await presignR2Url({ accountId: env.R2_ACCOUNT_ID, bucket: env.R2_BUCKET_NAME, key, accessKeyId: env.R2_ACCESS_KEY_ID, secretAccessKey: env.R2_SECRET_ACCESS_KEY, method: "PUT", signedHeaders: { "x-amz-meta-sha256": obj.oid }, }); const verifyUrl = new URL(request.url); verifyUrl.pathname = `/${repo}.git/info/lfs/objects/verify`; return { oid: obj.oid, size: obj.size, authenticated: true, actions: { upload: { href, header: { "x-amz-meta-sha256": obj.oid }, expires_in: 3600, }, verify: { href: verifyUrl.toString(), expires_in: 3600, }, }, }; }), ); return lfsJson({ transfer: "basic", objects: results }); } async function handleVerify(request: Request, env: Env, repo: string): Promise { const body = (await request.json()) as LfsVerifyRequest; const { oid, size } = body; if (!OID_RE.test(oid)) { return lfsError(422, "Invalid OID"); } const key = `${repo}/${oid}`; const head = await env.LFS_BUCKET.head(key); if (!head) { return lfsError(404, "Object not found"); } if (head.size !== size) { return lfsError(422, `Size mismatch: expected ${size}, got ${head.size}`); } if (head.customMetadata?.sha256 !== oid) { return lfsError(422, "OID mismatch in metadata"); } return new Response(null, { status: 200 }); } export default { async fetch(request: Request, env: Env): Promise { if (request.method !== "POST") { return lfsError(405, "Method not allowed"); } const url = new URL(request.url); const match = ROUTE_RE.exec(url.pathname); if (!match) { return lfsError(404, "Not found"); } const [, repo, action] = match; if (!authenticate(request, env)) { return new Response(JSON.stringify({ message: "Credentials needed" }), { status: 401, headers: { "Content-Type": "application/vnd.git-lfs+json", "WWW-Authenticate": "Basic", "LFS-Authenticate": "Basic realm=\"Git LFS\"", }, }); } if (action === "batch") { return handleBatch(request, env, repo); } return handleVerify(request, env, repo); }, } satisfies ExportedHandler;