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>
This commit is contained in:
205
host/server.mjs
Normal file
205
host/server.mjs
Normal file
@@ -0,0 +1,205 @@
|
||||
#!/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);
|
||||
85
host/start.sh
Executable file
85
host/start.sh
Executable file
@@ -0,0 +1,85 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
state_dir="$script_dir/state"
|
||||
pid_file="$state_dir/server.pid"
|
||||
log_file="$state_dir/server.log"
|
||||
token_file="$state_dir/token"
|
||||
config_file="$state_dir/config.env"
|
||||
|
||||
host="${GIT_CRED_PROXY_HOST:-127.0.0.1}"
|
||||
port="${GIT_CRED_PROXY_PORT:-18765}"
|
||||
protocols="${GIT_CRED_PROXY_PROTOCOLS:-https}"
|
||||
allowed_hosts="${GIT_CRED_PROXY_ALLOWED_HOSTS:-}"
|
||||
public_url="${GIT_CRED_PROXY_PUBLIC_URL:-http://host.docker.internal:${port}}"
|
||||
|
||||
mkdir -p "$state_dir"
|
||||
|
||||
if [ -f "$pid_file" ]; then
|
||||
old_pid=$(cat "$pid_file" 2>/dev/null || true)
|
||||
if [ -n "${old_pid:-}" ] && kill -0 "$old_pid" 2>/dev/null; then
|
||||
printf 'Proxy already running: pid=%s\n' "$old_pid"
|
||||
printf 'Container URL: %s\n' "$public_url"
|
||||
printf 'Token file: %s\n' "$token_file"
|
||||
exit 0
|
||||
fi
|
||||
rm -f "$pid_file"
|
||||
fi
|
||||
|
||||
if [ ! -f "$token_file" ]; then
|
||||
if ! command -v openssl >/dev/null 2>&1; then
|
||||
printf 'openssl is required to create the token file\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
umask 077
|
||||
openssl rand -hex 32 > "$token_file"
|
||||
fi
|
||||
|
||||
runtime=''
|
||||
if [ -n "${GIT_CRED_PROXY_RUNTIME:-}" ]; then
|
||||
runtime="$GIT_CRED_PROXY_RUNTIME"
|
||||
elif command -v bun >/dev/null 2>&1; then
|
||||
runtime='bun'
|
||||
elif command -v node >/dev/null 2>&1; then
|
||||
runtime='node'
|
||||
else
|
||||
printf 'Either bun or node is required to start the proxy\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
token=$(tr -d '\r\n' < "$token_file")
|
||||
|
||||
cat > "$config_file" <<EOF
|
||||
GIT_CRED_PROXY_HOST=$host
|
||||
GIT_CRED_PROXY_PORT=$port
|
||||
GIT_CRED_PROXY_PUBLIC_URL=$public_url
|
||||
GIT_CRED_PROXY_PROTOCOLS=$protocols
|
||||
GIT_CRED_PROXY_ALLOWED_HOSTS=$allowed_hosts
|
||||
EOF
|
||||
chmod 600 "$config_file"
|
||||
|
||||
GIT_CRED_PROXY_HOST="$host" \
|
||||
GIT_CRED_PROXY_PORT="$port" \
|
||||
GIT_CRED_PROXY_TOKEN="$token" \
|
||||
GIT_CRED_PROXY_PROTOCOLS="$protocols" \
|
||||
GIT_CRED_PROXY_ALLOWED_HOSTS="$allowed_hosts" \
|
||||
nohup "$runtime" "$script_dir/server.mjs" >>"$log_file" 2>&1 &
|
||||
|
||||
pid=$!
|
||||
printf '%s\n' "$pid" > "$pid_file"
|
||||
|
||||
sleep 1
|
||||
|
||||
if ! kill -0 "$pid" 2>/dev/null; then
|
||||
printf 'Proxy failed to start. Check %s\n' "$log_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'Proxy started\n'
|
||||
printf 'Host listen URL: http://%s:%s\n' "$host" "$port"
|
||||
printf 'Container URL: %s\n' "$public_url"
|
||||
printf 'Token file: %s\n' "$token_file"
|
||||
printf 'Log file: %s\n' "$log_file"
|
||||
printf 'Next: run /workspaces/host-git-cred-proxy/container/configure-git.sh inside the container\n'
|
||||
48
host/status.sh
Executable file
48
host/status.sh
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
state_dir="$script_dir/state"
|
||||
pid_file="$state_dir/server.pid"
|
||||
log_file="$state_dir/server.log"
|
||||
config_file="$state_dir/config.env"
|
||||
|
||||
host='127.0.0.1'
|
||||
port='18765'
|
||||
public_url="http://host.docker.internal:${port}"
|
||||
|
||||
if [ -f "$config_file" ]; then
|
||||
. "$config_file"
|
||||
host="${GIT_CRED_PROXY_HOST:-$host}"
|
||||
port="${GIT_CRED_PROXY_PORT:-$port}"
|
||||
public_url="${GIT_CRED_PROXY_PUBLIC_URL:-$public_url}"
|
||||
fi
|
||||
|
||||
printf 'Host listen URL: http://%s:%s\n' "$host" "$port"
|
||||
printf 'Container URL: %s\n' "$public_url"
|
||||
|
||||
if [ ! -f "$pid_file" ]; then
|
||||
printf 'Status: stopped\n'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pid=$(cat "$pid_file" 2>/dev/null || true)
|
||||
|
||||
if [ -z "${pid:-}" ] || ! kill -0 "$pid" 2>/dev/null; then
|
||||
printf 'Status: stale pid file\n'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'Status: running (pid=%s)\n' "$pid"
|
||||
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
if curl -fsS "http://${host}:${port}/healthz" >/dev/null 2>&1; then
|
||||
printf 'Health: ok\n'
|
||||
else
|
||||
printf 'Health: check failed\n'
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "$log_file" ]; then
|
||||
printf 'Log file: %s\n' "$log_file"
|
||||
fi
|
||||
27
host/stop.sh
Executable file
27
host/stop.sh
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
pid_file="$script_dir/state/server.pid"
|
||||
|
||||
if [ ! -f "$pid_file" ]; then
|
||||
printf 'Proxy is not running\n'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
pid=$(cat "$pid_file" 2>/dev/null || true)
|
||||
|
||||
if [ -z "${pid:-}" ]; then
|
||||
rm -f "$pid_file"
|
||||
printf 'Stale pid file removed\n'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
kill "$pid"
|
||||
printf 'Stopped proxy pid=%s\n' "$pid"
|
||||
else
|
||||
printf 'Proxy process was not running, removing stale pid file\n'
|
||||
fi
|
||||
|
||||
rm -f "$pid_file"
|
||||
Reference in New Issue
Block a user