feat(proxy): secure refactor and system-wide integration
- Removed hardcoded corporate proxy URL from all scripts. - Updated bridge.js to load configuration from /opt/proxy-bridge/config.json. - Updated setup.js to interactively configure upstream proxy and credentials. - Enhanced install_proxy.sh to automatically configure APT, Bash, and system services. - Purged sensitive URL from git history and verified zero leakage.
This commit is contained in:
+278
-40
@@ -1,57 +1,295 @@
|
||||
const http = require('http');
|
||||
const net = require('net');
|
||||
const { execSync } = require('child_process');
|
||||
const { execFileSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const vm = require('vm');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
// 1. CONFIGURATION
|
||||
const PROXY_HOST = 'PROXY_HOST_PLACEHOLDER';
|
||||
const PROXY_PORT = 8080;
|
||||
const CONFIG_FILE = process.env.PROXY_BRIDGE_CONFIG_FILE || '/opt/proxy-bridge/config.json';
|
||||
const USER_FILE = process.env.PROXY_BRIDGE_USER_FILE || '/opt/proxy-bridge/user.json';
|
||||
const PROFILE_FILE = '/opt/proxy-bridge/profile.json';
|
||||
const LISTEN_HOST = process.env.PROXY_BRIDGE_LISTEN_HOST || '127.0.0.1';
|
||||
const BASE_PORT = Number(process.env.PROXY_BRIDGE_BASE_PORT || 8888);
|
||||
|
||||
// 2. FETCH SECRETS FROM KEYRING
|
||||
let USER, PASS;
|
||||
let INTERNET_PROXY_HOST, INTERNET_PROXY_PORT;
|
||||
try {
|
||||
// Read the username we saved during setup
|
||||
USER = JSON.parse(fs.readFileSync('/opt/proxy-bridge/user.json')).username;
|
||||
// Query the Ubuntu Keyring for the password associated with this user/service
|
||||
PASS = execSync(`secret-tool lookup service proxy-bridge account ${USER}`).toString().trim();
|
||||
|
||||
if (!PASS) throw new Error("Password returned empty.");
|
||||
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
||||
INTERNET_PROXY_HOST = config.host;
|
||||
INTERNET_PROXY_PORT = Number(config.port);
|
||||
} catch (e) {
|
||||
console.error("CRITICAL: Could not retrieve credentials from keyring. Did you run setup.js?");
|
||||
console.error(e.message);
|
||||
process.exit(1);
|
||||
INTERNET_PROXY_HOST = 'PROXY_HOST_PLACEHOLDER';
|
||||
INTERNET_PROXY_PORT = 8080;
|
||||
}
|
||||
|
||||
// 3. GENERATE AUTH
|
||||
const AUTH_HEADER = 'Basic ' + Buffer.from(`${USER}:${PASS}`).toString('base64');
|
||||
let requestCounter = 0;
|
||||
let dynamicProfile = 'internet';
|
||||
const pacContexts = new Map(); // profileName -> vmContext
|
||||
|
||||
const server = http.createServer();
|
||||
function now() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
server.on('connect', (req, clientSocket, head) => {
|
||||
console.log(`--> Connecting to ${req.url}`);
|
||||
function log(level, id, message, fields = {}) {
|
||||
const suffix = Object.entries(fields)
|
||||
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
||||
.map(([key, value]) => `${key}=${JSON.stringify(value)}`)
|
||||
.join(' ');
|
||||
console.log(`${now()} ${level} [${id}] ${message}${suffix ? ` ${suffix}` : ''}`);
|
||||
}
|
||||
|
||||
const serverSocket = net.connect(PROXY_PORT, PROXY_HOST, () => {
|
||||
serverSocket.write(`CONNECT ${req.url} HTTP/1.1\r\n` +
|
||||
`Host: ${req.url}\r\n` +
|
||||
`Proxy-Authorization: ${AUTH_HEADER}\r\n` +
|
||||
`Proxy-Connection: Keep-Alive\r\n\r\n`);
|
||||
serverSocket.write(head);
|
||||
});
|
||||
function nextId(prefix) {
|
||||
requestCounter += 1;
|
||||
return `${prefix}-${requestCounter}`;
|
||||
}
|
||||
|
||||
serverSocket.once('data', (data) => {
|
||||
if (data.toString().includes('200')) {
|
||||
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
||||
serverSocket.pipe(clientSocket);
|
||||
clientSocket.pipe(serverSocket);
|
||||
} else {
|
||||
clientSocket.write(data);
|
||||
function loadCredentials() {
|
||||
try {
|
||||
const user = JSON.parse(fs.readFileSync(USER_FILE, 'utf8')).username;
|
||||
const pass = execFileSync('secret-tool', [
|
||||
'lookup',
|
||||
'service',
|
||||
'proxy-bridge',
|
||||
'account',
|
||||
user,
|
||||
], { encoding: 'utf8' }).trim();
|
||||
return pass ? { user, pass } : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
let AUTH_HEADER;
|
||||
function refreshAuth() {
|
||||
const creds = loadCredentials();
|
||||
if (creds) {
|
||||
AUTH_HEADER = 'Basic ' + Buffer.from(`${creds.user}:${creds.pass}`).toString('base64');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
refreshAuth();
|
||||
|
||||
function createPacSandbox() {
|
||||
const sandbox = {
|
||||
isPlainHostName: (host) => !host.includes('.'),
|
||||
dnsDomainIs: (host, domain) => host.endsWith(domain),
|
||||
localHostOrDomainIs: (host, hostdom) => host === hostdom || host.split('.')[0] === hostdom,
|
||||
isResolvable: (host) => {
|
||||
try { execFileSync('getent', ['hosts', host]); return true; } catch { return false; }
|
||||
},
|
||||
isInNet: (ip, pattern, mask) => false,
|
||||
dnsResolve: (host) => {
|
||||
try { return execFileSync('getent', ['hosts', host]).toString().split(/\s+/)[0]; } catch { return ""; }
|
||||
},
|
||||
myIpAddress: () => "127.0.0.1",
|
||||
dnsDomainLevels: (host) => host.split('.').length - 1,
|
||||
shExpMatch: (str, pattern) => {
|
||||
const p = pattern.replace(/\./g, '\\.').replace(/\*/g, '.*').replace(/\?/g, '.');
|
||||
return new RegExp('^' + p + '$').test(str);
|
||||
},
|
||||
weekdayRange: () => true,
|
||||
dateRange: () => true,
|
||||
timeRange: () => true,
|
||||
alert: (msg) => log('DEBUG', 'pac', msg),
|
||||
};
|
||||
return vm.createContext(sandbox);
|
||||
}
|
||||
|
||||
function loadPacFile(profileName) {
|
||||
try {
|
||||
const pacPath = path.join(os.homedir(), '.mozilla', `${profileName}.pac`);
|
||||
if (!fs.existsSync(pacPath)) return null;
|
||||
const script = fs.readFileSync(pacPath, 'utf8');
|
||||
const context = createPacSandbox();
|
||||
vm.runInContext(script, context);
|
||||
return typeof context.FindProxyForURL === 'function' ? context : null;
|
||||
} catch (e) {
|
||||
log('ERROR', 'pac', 'failed to load PAC', { profile: profileName, error: e.message });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getAvailableProfiles() {
|
||||
const profiles = ['internet', 'direct', 'off'];
|
||||
const pacDir = path.join(os.homedir(), '.mozilla');
|
||||
if (fs.existsSync(pacDir)) {
|
||||
fs.readdirSync(pacDir).forEach(f => {
|
||||
if (f.endsWith('.pac')) profiles.push(f.slice(0, -4));
|
||||
});
|
||||
}
|
||||
return profiles;
|
||||
}
|
||||
|
||||
function refreshPacContexts() {
|
||||
const all = getAvailableProfiles();
|
||||
all.forEach(name => {
|
||||
if (!['internet', 'direct', 'off'].includes(name)) {
|
||||
pacContexts.set(name, loadPacFile(name));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
serverSocket.on('error', () => clientSocket.end());
|
||||
clientSocket.on('error', () => serverSocket.end());
|
||||
function updateDynamicProfile() {
|
||||
try {
|
||||
if (fs.existsSync(PROFILE_FILE)) {
|
||||
const data = JSON.parse(fs.readFileSync(PROFILE_FILE, 'utf8'));
|
||||
const newProfile = data.profile || 'internet';
|
||||
if (newProfile !== dynamicProfile) {
|
||||
log('INFO', 'profile', 'dynamic profile switch', { old: dynamicProfile, new: newProfile });
|
||||
dynamicProfile = newProfile;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
fs.watchFile(PROFILE_FILE, { interval: 1000 }, updateDynamicProfile);
|
||||
updateDynamicProfile();
|
||||
refreshPacContexts();
|
||||
|
||||
function getUpstream(profileName, url, host) {
|
||||
const profile = (profileName === 'dynamic') ? dynamicProfile : profileName;
|
||||
|
||||
if (profile === 'off') return 'OFF';
|
||||
if (profile === 'direct') return 'DIRECT';
|
||||
if (profile === 'internet') return `PROXY ${INTERNET_PROXY_HOST}:${INTERNET_PROXY_PORT}`;
|
||||
|
||||
const ctx = pacContexts.get(profile);
|
||||
if (ctx) {
|
||||
try { return ctx.FindProxyForURL(url, host) || 'DIRECT'; }
|
||||
catch (e) { log('ERROR', 'pac', 'exec error', { profile, error: e.message }); return 'DIRECT'; }
|
||||
}
|
||||
return 'DIRECT';
|
||||
}
|
||||
|
||||
function parseProxyConfig(config) {
|
||||
const first = config.split(';')[0].trim();
|
||||
if (first.startsWith('PROXY ')) {
|
||||
const [host, port] = first.substring(6).split(':');
|
||||
return { type: 'PROXY', host, port: Number(port) };
|
||||
}
|
||||
return { type: first === 'OFF' ? 'OFF' : 'DIRECT' };
|
||||
}
|
||||
|
||||
function sanitizeProxyHeaders(headers) {
|
||||
const clean = { ...headers };
|
||||
['proxy-authorization', 'proxy-authenticate', 'proxy-connection', 'connection', 'via'].forEach(h => delete clean[h]);
|
||||
return clean;
|
||||
}
|
||||
|
||||
function parseUpstreamStatus(buffer) {
|
||||
const firstLine = buffer.toString('latin1').split('\r\n', 1)[0];
|
||||
const match = firstLine.match(/^HTTP\/\d(?:\.\d)?\s+(\d{3})\s*(.*)$/i);
|
||||
return { line: firstLine, code: match ? Number(match[1]) : undefined, text: match ? match[2] : undefined };
|
||||
}
|
||||
|
||||
function createBridge(port, profileName) {
|
||||
const server = http.createServer((clientReq, clientRes) => {
|
||||
const id = nextId('http');
|
||||
let urlObj;
|
||||
try { urlObj = new URL(clientReq.url); } catch { urlObj = new URL(clientReq.url, `http://${clientReq.headers.host}`); }
|
||||
|
||||
const upstreamConfig = getUpstream(profileName, urlObj.href, urlObj.hostname);
|
||||
const upstream = parseProxyConfig(upstreamConfig);
|
||||
|
||||
log('INFO', id, 'request', { port, profile: profileName, active: (profileName === 'dynamic' ? dynamicProfile : undefined), target: urlObj.href, upstream: upstreamConfig });
|
||||
|
||||
if (upstream.type === 'OFF') {
|
||||
clientRes.writeHead(403); return clientRes.end('Forbidden by proxy profile\n');
|
||||
}
|
||||
|
||||
const options = {
|
||||
method: clientReq.method,
|
||||
headers: sanitizeProxyHeaders(clientReq.headers),
|
||||
host: upstream.type === 'PROXY' ? upstream.host : urlObj.hostname,
|
||||
port: upstream.type === 'PROXY' ? upstream.port : (urlObj.port || 80),
|
||||
path: upstream.type === 'PROXY' ? urlObj.href : (urlObj.pathname + urlObj.search)
|
||||
};
|
||||
if (upstream.type === 'PROXY') {
|
||||
if (!AUTH_HEADER) refreshAuth();
|
||||
if (AUTH_HEADER) options.headers['Proxy-Authorization'] = AUTH_HEADER;
|
||||
}
|
||||
|
||||
const upstreamReq = http.request(options, (upstreamRes) => {
|
||||
clientRes.writeHead(upstreamRes.statusCode, sanitizeProxyHeaders(upstreamRes.headers));
|
||||
upstreamRes.pipe(clientRes);
|
||||
});
|
||||
upstreamReq.on('error', (e) => {
|
||||
if (!clientRes.headersSent) clientRes.writeHead(502);
|
||||
clientRes.end('Bad Gateway\n');
|
||||
});
|
||||
clientReq.pipe(upstreamReq);
|
||||
});
|
||||
|
||||
server.on('connect', (req, clientSocket, head) => {
|
||||
const id = nextId('connect');
|
||||
const [host, portStr] = req.url.split(':');
|
||||
const portNum = Number(portStr || 443);
|
||||
const upstreamConfig = getUpstream(profileName, `https://${req.url}/`, host);
|
||||
const upstream = parseProxyConfig(upstreamConfig);
|
||||
|
||||
log('INFO', id, 'connect', { port, profile: profileName, target: req.url, upstream: upstreamConfig });
|
||||
|
||||
if (upstream.type === 'OFF') {
|
||||
return clientSocket.end('HTTP/1.1 403 Forbidden\r\n\r\n');
|
||||
}
|
||||
|
||||
const upstreamSocket = net.connect(upstream.type === 'PROXY' ? upstream.port : portNum, upstream.type === 'PROXY' ? upstream.host : host, () => {
|
||||
if (upstream.type === 'PROXY') {
|
||||
if (!AUTH_HEADER) refreshAuth();
|
||||
upstreamSocket.write(`CONNECT ${req.url} HTTP/1.1\r\nHost: ${req.url}\r\n`);
|
||||
if (AUTH_HEADER) upstreamSocket.write(`Proxy-Authorization: ${AUTH_HEADER}\r\n`);
|
||||
upstreamSocket.write('\r\n');
|
||||
}
|
||||
if (head && head.length > 0 && upstream.type === 'DIRECT') upstreamSocket.write(head);
|
||||
});
|
||||
|
||||
let connected = false;
|
||||
upstreamSocket.on('data', (data) => {
|
||||
if (connected) return;
|
||||
if (upstream.type === 'PROXY') {
|
||||
const status = parseUpstreamStatus(data);
|
||||
if (status.code === 200) {
|
||||
connected = true;
|
||||
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
||||
const headerEnd = data.indexOf('\r\n\r\n');
|
||||
if (headerEnd !== -1 && data.length > headerEnd + 4) clientSocket.write(data.subarray(headerEnd + 4));
|
||||
upstreamSocket.pipe(clientSocket); clientSocket.pipe(upstreamSocket);
|
||||
} else {
|
||||
clientSocket.write(data); clientSocket.end(); upstreamSocket.end();
|
||||
}
|
||||
} else {
|
||||
connected = true;
|
||||
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
||||
clientSocket.write(data);
|
||||
upstreamSocket.pipe(clientSocket);
|
||||
clientSocket.pipe(upstreamSocket);
|
||||
}
|
||||
});
|
||||
|
||||
if (upstream.type === 'DIRECT') {
|
||||
upstreamSocket.on('connect', () => {
|
||||
if (connected) return;
|
||||
connected = true;
|
||||
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
||||
upstreamSocket.pipe(clientSocket);
|
||||
clientSocket.pipe(upstreamSocket);
|
||||
});
|
||||
}
|
||||
upstreamSocket.on('error', () => clientSocket.end('HTTP/1.1 502 Bad Gateway\r\n\r\n'));
|
||||
clientSocket.on('error', () => upstreamSocket.end());
|
||||
});
|
||||
|
||||
server.listen(port, LISTEN_HOST, () => {
|
||||
log('INFO', 'startup', 'bridge listener active', { port, profile: profileName });
|
||||
});
|
||||
}
|
||||
|
||||
// 1. Dynamic bridge on 8888
|
||||
createBridge(BASE_PORT, 'dynamic');
|
||||
|
||||
// 2. Static bridges on 8889+
|
||||
const allProfiles = getAvailableProfiles();
|
||||
allProfiles.forEach((name, i) => {
|
||||
createBridge(BASE_PORT + 1 + i, name);
|
||||
});
|
||||
|
||||
server.listen(8888, '127.0.0.1', () => {
|
||||
console.log('Bridge active on http://127.0.0.1:8888 (Auth via Keyring)');
|
||||
});
|
||||
@@ -1,48 +1,68 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Ensure the script is NOT run as root, so the user-level systemd service configures correctly
|
||||
# --- HELPER FUNCTIONS ---
|
||||
function info() { echo -e "\e[34mINFO:\e[0m $1"; }
|
||||
function warn() { echo -e "\e[33mWARN:\e[0m $1"; }
|
||||
function error() { echo -e "\e[31mERROR:\e[0m $1"; exit 1; }
|
||||
function success() { echo -e "\e[32mSUCCESS:\e[0m $1"; }
|
||||
|
||||
# 1. PREREQUISITE CHECKS
|
||||
info "Verifying prerequisites..."
|
||||
|
||||
if [ "$EUID" -eq 0 ]; then
|
||||
echo "❌ Please run this script as your standard user, not as root."
|
||||
echo "The script will prompt for sudo access automatically when needed."
|
||||
exit 1
|
||||
error "Please run this script as your standard user, not as root.\nThe script will prompt for sudo access automatically when needed."
|
||||
fi
|
||||
|
||||
echo "=== Proxy Bridge Installer ==="
|
||||
|
||||
# 1. Verify files exist
|
||||
if [ ! -f "bridge.js" ] || [ ! -f "setup.js" ]; then
|
||||
echo "❌ Error: bridge.js and/or setup.js not found in the current directory."
|
||||
echo "Please place this script in the same folder as your Node.js scripts."
|
||||
exit 1
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
error "Node.js is not installed. Please install Node.js first."
|
||||
fi
|
||||
|
||||
# 2. Install dependencies (requires sudo)
|
||||
echo "--> Installing required system packages (libsecret-tools)..."
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libsecret-tools
|
||||
# Check for files
|
||||
[ -f "bridge.js" ] || error "bridge.js not found."
|
||||
[ -f "setup.js" ] || error "setup.js not found."
|
||||
[ -f "proxyctl.js" ] || error "proxyctl.js not found."
|
||||
|
||||
# 3. Setup application directory (requires sudo)
|
||||
echo "--> Creating /opt/proxy-bridge directory..."
|
||||
sudo mkdir -p /opt/proxy-bridge
|
||||
sudo chown -R $USER:$USER /opt/proxy-bridge
|
||||
# 2. SYSTEM PACKAGES
|
||||
info "Installing required system packages (libsecret-tools)..."
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y libsecret-tools -qq
|
||||
|
||||
# 4. Copy files
|
||||
echo "--> Copying scripts to /opt/proxy-bridge..."
|
||||
cp bridge.js /opt/proxy-bridge/
|
||||
cp setup.js /opt/proxy-bridge/
|
||||
# 3. DIRECTORY SETUP
|
||||
BRIDGE_DIR="/opt/proxy-bridge"
|
||||
BIN_DIR="${HOME}/.local/bin"
|
||||
info "Setting up $BRIDGE_DIR..."
|
||||
sudo mkdir -p "$BRIDGE_DIR"
|
||||
sudo chown -R $USER:$USER "$BRIDGE_DIR"
|
||||
|
||||
# 5. Setup User-Level systemd Service
|
||||
echo "--> Configuring user-level systemd service..."
|
||||
# 4. DEPLOY FILES
|
||||
info "Deploying bridge scripts..."
|
||||
cp bridge.js "$BRIDGE_DIR/"
|
||||
cp setup.js "$BRIDGE_DIR/"
|
||||
cp proxyctl.js "$BRIDGE_DIR/"
|
||||
chmod +x "$BRIDGE_DIR/proxyctl.js"
|
||||
|
||||
info "Installing management CLI to $BIN_DIR/proxy-bridge..."
|
||||
mkdir -p "$BIN_DIR"
|
||||
ln -sf "$BRIDGE_DIR/proxyctl.js" "$BIN_DIR/proxy-bridge"
|
||||
|
||||
# 5. CONFIGURATION & CREDENTIALS
|
||||
if [ ! -f "$BRIDGE_DIR/user.json" ]; then
|
||||
info "Launching interactive setup..."
|
||||
node "$BRIDGE_DIR/setup.js"
|
||||
fi
|
||||
|
||||
# 6. SYSTEMD SERVICE SETUP
|
||||
info "Configuring user-level systemd service..."
|
||||
mkdir -p ~/.config/systemd/user/
|
||||
|
||||
cat <<EOF > ~/.config/systemd/user/proxy-bridge.service
|
||||
[Unit]
|
||||
Description=Dumb Pipe Proxy Bridge (Keyring Auth)
|
||||
Description=Local Proxy Bridge (Keyring Auth)
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/node /opt/proxy-bridge/bridge.js
|
||||
ExecStart=$(command -v node) $BRIDGE_DIR/bridge.js
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
@@ -50,21 +70,58 @@ RestartSec=5
|
||||
WantedBy=default.target
|
||||
EOF
|
||||
|
||||
# 6. Enable the service
|
||||
echo "--> Reloading systemd and enabling service..."
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable proxy-bridge.service
|
||||
systemctl --user restart proxy-bridge.service
|
||||
|
||||
echo ""
|
||||
echo "✅ Installation Complete!"
|
||||
echo "=================================================="
|
||||
echo "Next Steps:"
|
||||
echo "1. Run the interactive setup to store your proxy credentials:"
|
||||
echo " node /opt/proxy-bridge/setup.js"
|
||||
echo ""
|
||||
echo "2. Start the background service:"
|
||||
echo " systemctl --user start proxy-bridge.service"
|
||||
echo ""
|
||||
echo "3. Check the logs to ensure it's running smoothly:"
|
||||
echo " journalctl --user -u proxy-bridge.service -f"
|
||||
echo "=================================================="
|
||||
# 7. SYSTEM-WIDE PROXY CONFIGURATION (Point to local bridge)
|
||||
BRIDGE_URL="http://127.0.0.1:8888/"
|
||||
|
||||
# 7a. APT Configuration
|
||||
info "Updating APT proxy configuration..."
|
||||
APT_CONF="/etc/apt/apt.conf.d/80proxy-bridge"
|
||||
echo "Acquire::http::Proxy \"$BRIDGE_URL\";" | sudo tee "$APT_CONF" > /dev/null
|
||||
echo "Acquire::https::Proxy \"$BRIDGE_URL\";" | sudo tee -a "$APT_CONF" > /dev/null
|
||||
|
||||
# 7b. Bashrc Configuration
|
||||
info "Updating .bashrc proxy settings..."
|
||||
BASHRC="$HOME/.bashrc"
|
||||
MARKER="#PROXY_BRIDGE_CONFIG"
|
||||
|
||||
if ! grep -q "$MARKER" "$BASHRC"; then
|
||||
cat <<EOF >> "$BASHRC"
|
||||
|
||||
# Local Proxy Bridge Configuration $MARKER
|
||||
export http_proxy="$BRIDGE_URL"
|
||||
export https_proxy="$BRIDGE_URL"
|
||||
export HTTP_PROXY="$BRIDGE_URL"
|
||||
export HTTPS_PROXY="$BRIDGE_URL"
|
||||
export NO_PROXY="localhost,127.0.0.1,::1"
|
||||
export no_proxy="localhost,127.0.0.1,::1"
|
||||
EOF
|
||||
fi
|
||||
|
||||
# 7c. System Services (Docker, Snap, etc.)
|
||||
SERVICES=(docker snapd ollama)
|
||||
info "Updating system services proxy configuration..."
|
||||
|
||||
for service in "${SERVICES[@]}"; do
|
||||
if systemctl list-unit-files | grep -q "^${service}.service"; then
|
||||
info "Configuring $service..."
|
||||
SERVICE_DIR="/etc/systemd/system/${service}.service.d"
|
||||
sudo mkdir -p "$SERVICE_DIR"
|
||||
cat <<EOF | sudo tee "$SERVICE_DIR/http-proxy.conf" > /dev/null
|
||||
[Service]
|
||||
Environment="HTTP_PROXY=$BRIDGE_URL"
|
||||
Environment="HTTPS_PROXY=$BRIDGE_URL"
|
||||
Environment="NO_PROXY=localhost,127.0.0.1,::1"
|
||||
EOF
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl restart "$service" 2>/dev/null || warn "Failed to restart $service (maybe not running?)"
|
||||
fi
|
||||
done
|
||||
|
||||
success "Installation and system-wide configuration complete!"
|
||||
info "Management command: proxy-bridge status|toggle|profile"
|
||||
info "The Proxy Bridge is running and configured for APT, Bash, and core services."
|
||||
info "Logs: journalctl --user -u proxy-bridge.service -f"
|
||||
|
||||
@@ -0,0 +1,445 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const net = require('net');
|
||||
const { spawn, spawnSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const SERVICE = 'proxy-bridge.service';
|
||||
const LISTEN_HOST = process.env.PROXY_BRIDGE_LISTEN_HOST || '127.0.0.1';
|
||||
const LISTEN_PORT = Number(process.env.PROXY_BRIDGE_LISTEN_PORT || 8888);
|
||||
const USER_FILE = process.env.PROXY_BRIDGE_USER_FILE || '/opt/proxy-bridge/user.json';
|
||||
const CONFIG_FILE = process.env.PROXY_BRIDGE_CONFIG_FILE || '/opt/proxy-bridge/config.json';
|
||||
const PROFILE_FILE = '/opt/proxy-bridge/profile.json';
|
||||
const LOCAL_PROXY_URL = `http://${LISTEN_HOST}:${LISTEN_PORT}`;
|
||||
const VSCODE_CERT_FLAG = '';
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
return spawnSync(command, args, {
|
||||
encoding: 'utf8',
|
||||
stdio: options.stdio || 'pipe',
|
||||
});
|
||||
}
|
||||
|
||||
function getUpstreamConfig() {
|
||||
try {
|
||||
if (fs.existsSync(CONFIG_FILE)) {
|
||||
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
||||
return `${config.host}:${config.port}`;
|
||||
}
|
||||
} catch {}
|
||||
return 'PROXY_HOST_PLACEHOLDER:8080 (default)';
|
||||
}
|
||||
|
||||
function getProfile() {
|
||||
try {
|
||||
if (fs.existsSync(PROFILE_FILE)) {
|
||||
return JSON.parse(fs.readFileSync(PROFILE_FILE, 'utf8')).profile || 'internet';
|
||||
}
|
||||
} catch {}
|
||||
return 'internet';
|
||||
}
|
||||
|
||||
function getAvailableProfiles() {
|
||||
const profiles = ['internet', 'direct', 'off'];
|
||||
const pacDir = path.join(os.homedir(), '.mozilla');
|
||||
if (fs.existsSync(pacDir)) {
|
||||
const files = fs.readdirSync(pacDir);
|
||||
for (const f of files) {
|
||||
if (f.endsWith('.pac')) {
|
||||
profiles.push(f.slice(0, -4));
|
||||
}
|
||||
}
|
||||
}
|
||||
return profiles;
|
||||
}
|
||||
|
||||
function setProfile(profile) {
|
||||
const valid = getAvailableProfiles();
|
||||
if (!valid.includes(profile)) {
|
||||
console.error(`Invalid profile: ${profile}. Valid options: ${valid.join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
fs.mkdirSync(path.dirname(PROFILE_FILE), { recursive: true });
|
||||
fs.writeFileSync(PROFILE_FILE, JSON.stringify({ profile }));
|
||||
console.log(`Profile set to: ${profile}`);
|
||||
}
|
||||
|
||||
function systemctl(args, options = {}) {
|
||||
return run('systemctl', ['--user', ...args], options);
|
||||
}
|
||||
|
||||
function printCommandFailure(result) {
|
||||
if (result.error) {
|
||||
console.error(result.error.message);
|
||||
return;
|
||||
}
|
||||
if (result.stderr) {
|
||||
console.error(result.stderr.trim());
|
||||
}
|
||||
}
|
||||
|
||||
function serviceValue(property) {
|
||||
const result = systemctl(['show', SERVICE, '--property', property, '--value']);
|
||||
if (result.status !== 0) {
|
||||
return '';
|
||||
}
|
||||
return result.stdout.trim();
|
||||
}
|
||||
|
||||
function serviceState(command) {
|
||||
const result = systemctl([command, SERVICE]);
|
||||
if (result.status !== 0) {
|
||||
return 'unknown';
|
||||
}
|
||||
return result.stdout.trim() || 'unknown';
|
||||
}
|
||||
|
||||
function checkPort() {
|
||||
return new Promise((resolve) => {
|
||||
const socket = net.connect({ host: LISTEN_HOST, port: LISTEN_PORT });
|
||||
const timer = setTimeout(() => {
|
||||
socket.destroy();
|
||||
resolve(false);
|
||||
}, 1000);
|
||||
|
||||
socket.once('connect', () => {
|
||||
clearTimeout(timer);
|
||||
socket.end();
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
socket.once('error', () => {
|
||||
clearTimeout(timer);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function readAccount() {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(USER_FILE, 'utf8')).username || 'unknown';
|
||||
} catch {
|
||||
return 'not configured';
|
||||
}
|
||||
}
|
||||
|
||||
async function status() {
|
||||
const active = serviceState('is-active');
|
||||
const enabled = serviceState('is-enabled');
|
||||
const pid = serviceValue('MainPID');
|
||||
const listening = await checkPort();
|
||||
const profile = getProfile();
|
||||
const all = getAvailableProfiles();
|
||||
|
||||
console.log(`service: ${SERVICE}`);
|
||||
console.log(`active: ${active}`);
|
||||
console.log(`enabled: ${enabled}`);
|
||||
console.log(`pid: ${pid && pid !== '0' ? pid : 'none'}`);
|
||||
console.log(`profile: ${profile}`);
|
||||
console.log(`available: ${all.join(', ')}`);
|
||||
console.log(`listen: http://${LISTEN_HOST}:${LISTEN_PORT} (${listening ? 'reachable' : 'not reachable'})`);
|
||||
console.log(`upstream: ${getUpstreamConfig()}`);
|
||||
console.log(`account: ${readAccount()}`);
|
||||
}
|
||||
|
||||
function control(action) {
|
||||
const result = systemctl([action, SERVICE], { stdio: 'inherit' });
|
||||
if (result.status !== 0) {
|
||||
printCommandFailure(result);
|
||||
process.exit(result.status || 1);
|
||||
}
|
||||
}
|
||||
|
||||
function logs() {
|
||||
const child = spawn('journalctl', ['--user', '-u', SERVICE, '-f'], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
child.on('exit', (code) => process.exit(code || 0));
|
||||
}
|
||||
|
||||
function setup() {
|
||||
const child = spawn('node', ['/opt/proxy-bridge/setup.js'], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
child.on('exit', (code) => process.exit(code || 0));
|
||||
}
|
||||
|
||||
function config() {
|
||||
console.log(`listen: ${LOCAL_PROXY_URL}`);
|
||||
console.log(`upstream: ${getUpstreamConfig()}`);
|
||||
console.log(`profile: ${getProfile()}`);
|
||||
console.log(`available profiles: ${getAvailableProfiles().join(', ')}`);
|
||||
console.log(`user_file: ${USER_FILE}`);
|
||||
console.log(`account: ${readAccount()}`);
|
||||
}
|
||||
|
||||
function codeSettingsPath() {
|
||||
const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
||||
return path.join(configHome, 'Code', 'User', 'settings.json');
|
||||
}
|
||||
|
||||
function readJsonFile(file) {
|
||||
if (!fs.existsSync(file)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(file, 'utf8').trim();
|
||||
if (!content) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
function writeJsonFile(file, data) {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, `${JSON.stringify(data, null, 4)}\n`);
|
||||
}
|
||||
|
||||
function desktopApplicationsDir() {
|
||||
const dataHome = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share');
|
||||
return path.join(dataHome, 'applications');
|
||||
}
|
||||
|
||||
function desktopLauncherPaths() {
|
||||
const userDir = desktopApplicationsDir();
|
||||
return [
|
||||
{
|
||||
name: 'main',
|
||||
system: '/usr/share/applications/code.desktop',
|
||||
user: path.join(userDir, 'code.desktop'),
|
||||
},
|
||||
{
|
||||
name: 'url-handler',
|
||||
system: '/usr/share/applications/code-url-handler.desktop',
|
||||
user: path.join(userDir, 'code-url-handler.desktop'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function addCodeFlag(command) {
|
||||
if (command.includes(VSCODE_CERT_FLAG)) {
|
||||
return command;
|
||||
}
|
||||
|
||||
return command.replace(/^(Exec=\S+)/, `$1 ${VSCODE_CERT_FLAG}`);
|
||||
}
|
||||
|
||||
function launcherHasFlag(file) {
|
||||
if (!fs.existsSync(file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return fs.readFileSync(file, 'utf8').includes(VSCODE_CERT_FLAG);
|
||||
}
|
||||
|
||||
function vscodeLaunchStatus() {
|
||||
for (const launcher of desktopLauncherPaths()) {
|
||||
const exists = fs.existsSync(launcher.user);
|
||||
const enabled = launcherHasFlag(launcher.user);
|
||||
console.log(`${launcher.name}: ${launcher.user}`);
|
||||
console.log(` user override: ${exists ? 'yes' : 'no'}`);
|
||||
console.log(` ${VSCODE_CERT_FLAG}: ${enabled ? 'yes' : 'no'}`);
|
||||
}
|
||||
|
||||
const userCode = path.join(os.homedir(), '.local', 'bin', 'code');
|
||||
console.log(`terminal wrapper: ${userCode}`);
|
||||
console.log(` installed: ${fs.existsSync(userCode) ? 'yes' : 'no'}`);
|
||||
console.log(` ${VSCODE_CERT_FLAG}: ${launcherHasFlag(userCode) ? 'yes' : 'no'}`);
|
||||
}
|
||||
|
||||
function userCodeWrapperPath() {
|
||||
return path.join(os.homedir(), '.local', 'bin', 'code');
|
||||
}
|
||||
|
||||
function vscodeTerminalSetup() {
|
||||
const file = userCodeWrapperPath();
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, `#!/bin/sh\nexec /usr/bin/code ${VSCODE_CERT_FLAG} "$@"\n`);
|
||||
fs.chmodSync(file, 0o755);
|
||||
console.log(`Updated ${file}`);
|
||||
}
|
||||
|
||||
function vscodeLaunchSetup() {
|
||||
fs.mkdirSync(desktopApplicationsDir(), { recursive: true });
|
||||
|
||||
for (const launcher of desktopLauncherPaths()) {
|
||||
if (!fs.existsSync(launcher.system)) {
|
||||
console.error(`Missing system launcher: ${launcher.system}`);
|
||||
process.exitCode = 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(launcher.system, 'utf8')
|
||||
.split('\n')
|
||||
.map((line) => (line.startsWith('Exec=') ? addCodeFlag(line) : line))
|
||||
.join('\n');
|
||||
|
||||
fs.writeFileSync(launcher.user, content);
|
||||
console.log(`Updated ${launcher.user}`);
|
||||
}
|
||||
|
||||
vscodeTerminalSetup();
|
||||
}
|
||||
|
||||
async function vscodeStatus() {
|
||||
const file = codeSettingsPath();
|
||||
let settings;
|
||||
|
||||
try {
|
||||
settings = readJsonFile(file);
|
||||
} catch (error) {
|
||||
console.error(`Could not parse ${file}: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`settings: ${file}`);
|
||||
console.log(`http.proxy: ${settings['http.proxy'] || 'not set'}`);
|
||||
console.log(`http.proxySupport: ${settings['http.proxySupport'] || 'not set'}`);
|
||||
console.log(`http.proxyStrictSSL: ${settings['http.proxyStrictSSL'] === undefined ? 'not set' : settings['http.proxyStrictSSL']}`);
|
||||
console.log(`http.systemCertificates: ${settings['http.systemCertificates'] === undefined ? 'not set' : settings['http.systemCertificates']}`);
|
||||
console.log(`local proxy reachable: ${await checkPort() ? 'yes' : 'no'}`);
|
||||
}
|
||||
|
||||
async function vscodeSetup() {
|
||||
const file = codeSettingsPath();
|
||||
let settings;
|
||||
|
||||
try {
|
||||
settings = readJsonFile(file);
|
||||
} catch (error) {
|
||||
console.error(`Could not parse ${file}: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
settings['http.proxy'] = LOCAL_PROXY_URL;
|
||||
settings['http.proxySupport'] = 'override';
|
||||
settings['http.proxyStrictSSL'] = false;
|
||||
settings['http.systemCertificates'] = true;
|
||||
|
||||
writeJsonFile(file, settings);
|
||||
|
||||
console.log(`Updated ${file}`);
|
||||
console.log(`http.proxy: ${LOCAL_PROXY_URL}`);
|
||||
console.log('http.proxySupport: override');
|
||||
console.log('http.proxyStrictSSL: false');
|
||||
console.log('http.systemCertificates: true');
|
||||
|
||||
const listening = await checkPort();
|
||||
console.log(`local proxy reachable: ${listening ? 'yes' : 'no'}`);
|
||||
}
|
||||
|
||||
async function vscode(args) {
|
||||
const action = args[0] || 'status';
|
||||
|
||||
switch (action) {
|
||||
case 'status':
|
||||
await vscodeStatus();
|
||||
break;
|
||||
case 'setup':
|
||||
await vscodeSetup();
|
||||
break;
|
||||
case 'launch':
|
||||
switch (args[1] || 'status') {
|
||||
case 'status':
|
||||
vscodeLaunchStatus();
|
||||
break;
|
||||
case 'setup':
|
||||
vscodeLaunchSetup();
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown vscode launch action: ${args[1]}`);
|
||||
console.error('Usage: proxy-bridge vscode launch [status|setup]');
|
||||
process.exit(1);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown vscode action: ${action}`);
|
||||
console.error('Usage: proxy-bridge vscode [status|setup|launch]');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function help() {
|
||||
console.log(`Usage: proxy-bridge <command>
|
||||
|
||||
Commands:
|
||||
status Show service, listener, upstream, profile, and account
|
||||
profile [name] Show or set the active routing profile
|
||||
toggle Cycle through profiles (internet -> direct -> off -> [pacs...])
|
||||
start Start the local proxy bridge
|
||||
stop Stop the local proxy bridge
|
||||
restart Restart the local proxy bridge
|
||||
enable Enable the user service at login
|
||||
disable Disable the user service at login
|
||||
logs Follow service logs
|
||||
setup Store or update keyring credentials
|
||||
config Print effective non-secret configuration
|
||||
vscode Manage VS Code proxy settings
|
||||
help Show this help
|
||||
|
||||
VS Code:
|
||||
proxy-bridge vscode status
|
||||
proxy-bridge vscode setup
|
||||
proxy-bridge vscode launch status
|
||||
proxy-bridge vscode launch setup
|
||||
`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const command = process.argv[2] || 'status';
|
||||
|
||||
switch (command) {
|
||||
case 'status':
|
||||
await status();
|
||||
break;
|
||||
case 'profile':
|
||||
if (process.argv[3]) {
|
||||
setProfile(process.argv[3]);
|
||||
} else {
|
||||
console.log(getProfile());
|
||||
}
|
||||
break;
|
||||
case 'toggle':
|
||||
const current = getProfile();
|
||||
const profiles = getAvailableProfiles();
|
||||
const next = profiles[(profiles.indexOf(current) + 1) % profiles.length];
|
||||
setProfile(next);
|
||||
break;
|
||||
case 'start':
|
||||
case 'stop':
|
||||
case 'restart':
|
||||
case 'enable':
|
||||
case 'disable':
|
||||
control(command);
|
||||
break;
|
||||
case 'logs':
|
||||
logs();
|
||||
break;
|
||||
case 'setup':
|
||||
setup();
|
||||
break;
|
||||
case 'config':
|
||||
config();
|
||||
break;
|
||||
case 'vscode':
|
||||
await vscode(process.argv.slice(3));
|
||||
break;
|
||||
case 'help':
|
||||
case '--help':
|
||||
case '-h':
|
||||
help();
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown command: ${command}`);
|
||||
help();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
+71
-24
@@ -1,34 +1,81 @@
|
||||
const readline = require('readline');
|
||||
const { execSync } = require('child_process');
|
||||
const { execFileSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
console.log("=== Proxy Bridge Keyring Setup ===");
|
||||
const CONFIG_DIR = '/opt/proxy-bridge';
|
||||
const CONFIG_FILE = `${CONFIG_DIR}/config.json`;
|
||||
const USER_FILE = `${CONFIG_DIR}/user.json`;
|
||||
|
||||
rl.question('Enter your corporate username: ', (user) => {
|
||||
rl.question('Enter your corporate password: ', (pass) => {
|
||||
try {
|
||||
// Securely store the password in the Ubuntu Keyring using secret-tool
|
||||
// We use standard input to pass the password so it doesn't appear in process lists
|
||||
execSync(`secret-tool store --label="Proxy Bridge Credentials" service proxy-bridge account ${user}`, {
|
||||
input: pass
|
||||
});
|
||||
|
||||
// Store the username in a local config just so the bridge knows WHICH account to look up
|
||||
require('fs').writeFileSync('/opt/proxy-bridge/user.json', JSON.stringify({ username: user }));
|
||||
console.log("=== Proxy Bridge Configuration Setup ===");
|
||||
|
||||
console.log("\n✅ Credentials successfully stored in the system keyring.");
|
||||
} catch (error) {
|
||||
console.error("\n❌ Failed to store credentials in keyring:", error.message);
|
||||
}
|
||||
rl.close();
|
||||
function ask(question, defaultValue = "") {
|
||||
return new Promise((resolve) => {
|
||||
const prompt = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `;
|
||||
rl.question(prompt, (answer) => {
|
||||
resolve(answer || defaultValue);
|
||||
});
|
||||
});
|
||||
|
||||
// Hide typing for password (basic implementation)
|
||||
rl._writeToOutput = function _writeToOutput(stringToWrite) {
|
||||
if (rl.history.length === 0) rl.output.write("*");
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
// 1. Upstream Proxy Configuration
|
||||
console.log("\n--- Upstream Proxy (Corporate) ---");
|
||||
const host = await ask("Corporate Proxy Hostname (e.g. proxy.company.com)");
|
||||
const port = await ask("Corporate Proxy Port", "8080");
|
||||
|
||||
if (!host) {
|
||||
console.error("❌ Hostname is required.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 2. Credentials
|
||||
console.log("\n--- Credentials ---");
|
||||
const user = await ask("Corporate Username");
|
||||
|
||||
// Setup password mask
|
||||
const oldWrite = rl._writeToOutput;
|
||||
rl._writeToOutput = function _writeToOutput(stringToWrite) {
|
||||
if (rl.line.length > 0) rl.output.write("*");
|
||||
else rl.output.write(stringToWrite);
|
||||
};
|
||||
const pass = await ask("Corporate Password");
|
||||
rl._writeToOutput = oldWrite;
|
||||
|
||||
// Save Proxy Config
|
||||
if (!fs.existsSync(CONFIG_DIR)) {
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ host, port }, null, 2));
|
||||
|
||||
// Save Username
|
||||
fs.writeFileSync(USER_FILE, JSON.stringify({ username: user }));
|
||||
|
||||
// Store Password in Keyring
|
||||
console.log("\n--> Storing password in system keyring...");
|
||||
execFileSync('secret-tool', [
|
||||
'store',
|
||||
'--label=Proxy Bridge Credentials',
|
||||
'service',
|
||||
'proxy-bridge',
|
||||
'account',
|
||||
user,
|
||||
], {
|
||||
input: pass
|
||||
});
|
||||
|
||||
console.log("\n✅ Configuration and credentials successfully stored.");
|
||||
} catch (error) {
|
||||
console.error("\n❌ Setup failed:", error.message);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
|
||||
Reference in New Issue
Block a user