pr_01m47d15m3e54sn21z27rpy5n9/services/repos/src/git-http.ts

88 lines2,476 bytesCodeBlame
1import {
2 type GitService,
3 type IdentityApi,
4 type RepoPath,
5 type ReposApi,
6 type Viewer,
7 httpStatus,
8} from "@g1t/contracts";
9
10const GIT_ROUTE =
11 /^\/([^/]+)\/([^/]+?)(?:\.git)?\/(info\/refs|git-upload-pack|git-receive-pack)$/;
12const FORWARDED_HEADERS = [
13 "accept",
14 "content-encoding",
15 "content-type",
16 "git-protocol",
17 "user-agent",
18];
19
20async function viewerFromBasicAuth(
21 request: Request,
22 identity: IdentityApi,
23): Promise<Viewer> {
24 const [scheme, encoded] = (request.headers.get("authorization") ?? "").split(" ");
25 if (scheme?.toLowerCase() !== "basic" || !encoded) return null;
26 let decoded: string;
27 try {
28 decoded = atob(encoded);
29 } catch {
30 return null;
31 }
32 const separator = decoded.indexOf(":");
33 if (separator < 0) return null;
34 return identity.userForGitCredentials(
35 decoded.slice(0, separator),
36 decoded.slice(separator + 1),
37 );
38}
39
40/**
41 * Smart HTTP git remote at `/<namespace>/<repo>.git`, proxied to the git
42 * store with a short-lived token. Returns null for requests that are not git.
43 */
44export async function handleGitHttp(
45 request: Request,
46 identity: IdentityApi,
47 repos: Pick<ReposApi, "gitAccess">,
48 onPush: (path: RepoPath) => void,
49): Promise<Response | null> {
50 const url = new URL(request.url);
51 const match = GIT_ROUTE.exec(url.pathname);
52 if (!match) return null;
53 const [, namespace, name, endpoint] = match;
54 const service =
55 endpoint === "info/refs" ? url.searchParams.get("service") : endpoint;
56 if (service !== "git-upload-pack" && service !== "git-receive-pack") {
57 return null;
58 }
59
60 const viewer = await viewerFromBasicAuth(request, identity);
61 const access = await repos.gitAccess(
62 { namespace, name },
63 viewer,
64 service as GitService,
65 );
66 if (!access.ok) {
67 const status = httpStatus(access.error);
68 return new Response(`${access.error.message}\n`, {
69 status,
70 headers: status === 401 ? { "www-authenticate": 'Basic realm="g1t"' } : {},
71 });
72 }
73
74 const headers = new Headers({ authorization: `Bearer ${access.value.token}` });
75 for (const header of FORWARDED_HEADERS) {
76 const value = request.headers.get(header);
77 if (value) headers.set(header, value);
78 }
79 const response = await fetch(`${access.value.remote}/${endpoint}${url.search}`, {
80 method: request.method,
81 headers,
82 body: request.body,
83 });
84 if (endpoint === "git-receive-pack" && response.ok) {
85 onPush({ namespace, name });
86 }
87 return response;
88}