Files
host-git-cred-proxy/host/server.mjs
严浩 f90673cb3a Reorganize host runtime files
Move the host-only proxy entrypoints into a dedicated directory so it is obvious which files run on the host.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-08 17:51:12 +08:00

206 lines
4.7 KiB
JavaScript

#!/usr/bin/env node
import http from 'node:http';
import process from 'node:process';
import { spawn } from 'node:child_process';
const host = process.env.GIT_CRED_PROXY_HOST ?? '127.0.0.1';
const port = Number(process.env.GIT_CRED_PROXY_PORT ?? '18765');
const token = process.env.GIT_CRED_PROXY_TOKEN ?? '';
if (!token) {
process.stderr.write('Missing GIT_CRED_PROXY_TOKEN\n');
process.exit(1);
}
const allowedProtocols = new Set(
(process.env.GIT_CRED_PROXY_PROTOCOLS ?? 'https')
.split(',')
.map((value) => value.trim().toLowerCase())
.filter(Boolean),
);
const allowedHosts = new Set(
(process.env.GIT_CRED_PROXY_ALLOWED_HOSTS ?? '')
.split(',')
.map((value) => value.trim())
.filter(Boolean),
);
const actionMap = new Map([
['get', 'fill'],
['store', 'approve'],
['erase', 'reject'],
['fill', 'fill'],
['approve', 'approve'],
['reject', 'reject'],
]);
function parseCredentialBody(body) {
const result = {};
for (const line of body.split('\n')) {
if (!line || !line.includes('=')) {
continue;
}
const index = line.indexOf('=');
const key = line.slice(0, index);
const value = line.slice(index + 1);
result[key] = value;
}
return result;
}
function readBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
let size = 0;
req.on('data', (chunk) => {
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
size += buffer.length;
if (size > 64 * 1024) {
reject(new Error('Request body too large'));
req.destroy();
return;
}
chunks.push(buffer);
});
req.on('end', () => {
resolve(Buffer.concat(chunks).toString('utf8'));
});
req.on('error', reject);
});
}
function runGitCredential(action, input) {
return new Promise((resolve, reject) => {
const child = spawn('git', ['credential', action], {
env: {
...process.env,
GIT_TERMINAL_PROMPT: '0',
},
stdio: ['pipe', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => {
stdout += chunk.toString();
});
child.stderr.on('data', (chunk) => {
stderr += chunk.toString();
});
child.on('error', reject);
child.on('close', (code) => {
resolve({ code: code ?? 1, stdout, stderr });
});
child.stdin.end(input);
});
}
function sendText(res, statusCode, body) {
res.writeHead(statusCode, {
'Content-Type': 'text/plain; charset=utf-8',
});
res.end(body);
}
function isMissingCredentialError(stderr) {
const normalized = stderr.toLowerCase();
return (
normalized.includes('terminal prompts disabled') ||
normalized.includes('could not read username') ||
normalized.includes('could not read password')
);
}
const server = http.createServer(async (req, res) => {
try {
const url = new URL(req.url ?? '/', 'http://localhost');
if (req.method === 'GET' && url.pathname === '/healthz') {
sendText(res, 200, 'ok\n');
return;
}
if (req.method !== 'POST') {
sendText(res, 405, 'Method Not Allowed\n');
return;
}
const authHeader = req.headers.authorization ?? '';
if (authHeader !== `Bearer ${token}`) {
sendText(res, 401, 'Unauthorized\n');
return;
}
const action = actionMap.get(url.pathname.replace(/^\//, ''));
if (!action) {
sendText(res, 404, 'Not Found\n');
return;
}
const body = await readBody(req);
const attrs = parseCredentialBody(body);
const protocol = attrs.protocol?.toLowerCase();
const requestHost = attrs.host;
if (allowedProtocols.size > 0) {
if (!protocol) {
sendText(res, 400, 'Missing protocol\n');
return;
}
if (!allowedProtocols.has(protocol)) {
sendText(res, 403, 'Protocol not allowed\n');
return;
}
}
if (allowedHosts.size > 0 && requestHost && !allowedHosts.has(requestHost)) {
sendText(res, 403, 'Host not allowed\n');
return;
}
const result = await runGitCredential(action, body);
if (result.code === 0) {
sendText(res, 200, result.stdout);
return;
}
if (action === 'fill' && isMissingCredentialError(result.stderr)) {
sendText(res, 200, '');
return;
}
sendText(res, 502, result.stderr || `git credential ${action} failed\n`);
} catch (error) {
sendText(res, 500, `${error instanceof Error ? error.message : String(error)}\n`);
}
});
server.listen(port, host, () => {
process.stdout.write(`Git credential proxy listening on http://${host}:${port}\n`);
});
function shutdown() {
server.close(() => {
process.exit(0);
});
}
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);