Browse Source

support tor relay and git server addresses

main
Silberengel 4 weeks ago
parent
commit
7cf3e33420
  1. 43
      README.md
  2. 34
      package-lock.json
  3. 1
      package.json
  4. 42
      src/lib/config.ts
  5. 80
      src/lib/services/git/repo-manager.ts
  6. 147
      src/lib/services/nostr/nostr-client.ts
  7. 83
      src/lib/services/tor/hidden-service.ts
  8. 38
      src/lib/utils/tor.ts
  9. 13
      src/routes/api/repos/[npub]/[repo]/settings/+server.ts
  10. 12
      src/routes/api/tor/onion/+server.ts
  11. 19
      src/routes/signup/+page.svelte

43
README.md

@ -348,6 +348,49 @@ See `docs/SECURITY.md` and `docs/SECURITY_IMPLEMENTATION.md` for detailed inform @@ -348,6 +348,49 @@ See `docs/SECURITY.md` and `docs/SECURITY_IMPLEMENTATION.md` for detailed inform
- `GIT_REPO_ROOT`: Path to store git repositories (default: `/repos`)
- `GIT_DOMAIN`: Domain for git repositories (default: `localhost:6543`)
- `NOSTR_RELAYS`: Comma-separated list of Nostr relays (default: `wss://theforest.nostr1.com,wss://nostr.land,wss://relay.damus.io`)
- `TOR_SOCKS_PROXY`: Tor SOCKS proxy address (format: `host:port`, default: `127.0.0.1:9050`). Set to empty string to disable Tor support. When configured, the server will automatically route `.onion` addresses through Tor for both Nostr relay connections and git operations.
- `TOR_ONION_ADDRESS`: Tor hidden service .onion address (optional). If not set, the server will attempt to read it from Tor's hostname file. When configured, every repository will automatically get a `.onion` clone URL in addition to the regular domain URL, making repositories accessible via Tor even if the server is only running on localhost.
### Tor Hidden Service Setup
To provide `.onion` addresses for all repositories, you need to set up a Tor hidden service:
1. **Install and configure Tor**:
```bash
# On Debian/Ubuntu
sudo apt-get install tor
# Edit Tor configuration
sudo nano /etc/tor/torrc
```
2. **Add hidden service configuration**:
```
HiddenServiceDir /var/lib/tor/gitrepublic
HiddenServicePort 80 127.0.0.1:6543
```
3. **Restart Tor**:
```bash
sudo systemctl restart tor
```
4. **Get your .onion address**:
```bash
sudo cat /var/lib/tor/gitrepublic/hostname
```
5. **Set environment variable** (optional, if hostname file is in a different location):
```bash
export TOR_ONION_ADDRESS=your-onion-address.onion
```
The server will automatically:
- Detect the `.onion` address from the hostname file or environment variable
- Add a `.onion` clone URL to every repository announcement
- Make repositories accessible via Tor even if the server is only on localhost
**Note**: The `.onion` address works even if your server is only accessible on `localhost` - Tor will handle the routing!
### Security Configuration

34
package-lock.json generated

@ -23,6 +23,7 @@ @@ -23,6 +23,7 @@
"pino": "^10.3.1",
"pino-pretty": "^13.1.3",
"simple-git": "^3.31.1",
"socks": "^2.8.0",
"svelte": "^5.0.0",
"ws": "^8.19.0"
},
@ -3251,6 +3252,15 @@ @@ -3251,6 +3252,15 @@
"dev": true,
"license": "ISC"
},
"node_modules/ip-address": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@ -4319,6 +4329,30 @@ @@ -4319,6 +4329,30 @@
"node": ">=8"
}
},
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
"license": "MIT",
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks": {
"version": "2.8.7",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
"license": "MIT",
"dependencies": {
"ip-address": "^10.0.1",
"smart-buffer": "^4.2.0"
},
"engines": {
"node": ">= 10.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/sonic-boom": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",

1
package.json

@ -27,6 +27,7 @@ @@ -27,6 +27,7 @@
"pino": "^10.3.1",
"pino-pretty": "^13.1.3",
"simple-git": "^3.31.1",
"socks": "^2.8.0",
"svelte": "^5.0.0",
"ws": "^8.19.0"
},

42
src/lib/config.ts

@ -48,6 +48,48 @@ export function getGitUrl(npub: string, repoName: string): string { @@ -48,6 +48,48 @@ export function getGitUrl(npub: string, repoName: string): string {
return `${protocol}://${GIT_DOMAIN}/${npub}/${repoName}.git`;
}
/**
* Tor SOCKS proxy configuration
* Defaults to localhost:9050 (standard Tor SOCKS port)
* Can be overridden by TOR_SOCKS_PROXY env var (format: host:port)
* Set to empty string to disable Tor support
*/
export const TOR_SOCKS_PROXY =
typeof process !== 'undefined' && process.env?.TOR_SOCKS_PROXY !== undefined
? process.env.TOR_SOCKS_PROXY.trim()
: '127.0.0.1:9050';
export const TOR_ENABLED = TOR_SOCKS_PROXY !== '';
/**
* Parse Tor SOCKS proxy into host and port
*/
export function parseTorProxy(): { host: string; port: number } | null {
if (!TOR_ENABLED) return null;
const [host, portStr] = TOR_SOCKS_PROXY.split(':');
const port = parseInt(portStr || '9050', 10);
if (!host || isNaN(port)) {
return null;
}
return { host, port };
}
/**
* Check if a URL is a .onion address
*/
export function isOnionAddress(url: string): boolean {
try {
const urlObj = new URL(url);
return urlObj.hostname.endsWith('.onion');
} catch {
// If URL parsing fails, check if it contains .onion
return url.includes('.onion');
}
}
/**
* Combine default relays with user's relays (from kind 10002)
* Returns a deduplicated list with user relays first, then defaults

80
src/lib/services/git/repo-manager.ts

@ -13,6 +13,7 @@ import { GIT_DOMAIN } from '../../config.js'; @@ -13,6 +13,7 @@ import { GIT_DOMAIN } from '../../config.js';
import { generateVerificationFile, VERIFICATION_FILE_PATH } from '../nostr/repo-verification.js';
import simpleGit, { type SimpleGit } from 'simple-git';
import logger from '../logger.js';
import { shouldUseTor, getTorProxy } from '../../utils/tor.js';
const execAsync = promisify(exec);
@ -104,6 +105,41 @@ export class RepoManager { @@ -104,6 +105,41 @@ export class RepoManager {
}
}
/**
* Get git environment variables with Tor proxy if needed for .onion addresses
*/
private getGitEnvForUrl(url: string): Record<string, string> {
const env = { ...process.env };
if (shouldUseTor(url)) {
const proxy = getTorProxy();
if (proxy) {
// Git uses GIT_PROXY_COMMAND for proxy support
// The command receives host and port as arguments
// We'll create a simple proxy command using socat or nc
// Note: This requires socat or netcat-openbsd to be installed
const proxyCommand = `sh -c 'exec socat - SOCKS5:${proxy.host}:${proxy.port}:\\$1:\\$2' || sh -c 'exec nc -X 5 -x ${proxy.host}:${proxy.port} \\$1 \\$2'`;
env.GIT_PROXY_COMMAND = proxyCommand;
// Also set ALL_PROXY for git-remote-http
env.ALL_PROXY = `socks5://${proxy.host}:${proxy.port}`;
// For HTTP/HTTPS URLs, also set http_proxy and https_proxy
try {
const urlObj = new URL(url);
if (urlObj.protocol === 'http:' || urlObj.protocol === 'https:') {
env.http_proxy = `socks5://${proxy.host}:${proxy.port}`;
env.https_proxy = `socks5://${proxy.host}:${proxy.port}`;
}
} catch {
// URL parsing failed, skip proxy env vars
}
}
}
return env;
}
/**
* Sync repository from multiple remote URLs
*/
@ -114,11 +150,27 @@ export class RepoManager { @@ -114,11 +150,27 @@ export class RepoManager {
const remoteName = `remote-${remoteUrls.indexOf(url)}`;
await execAsync(`cd "${repoPath}" && git remote add ${remoteName} "${url}" || true`);
// Fetch from remote
await execAsync(`cd "${repoPath}" && git fetch ${remoteName} --all`);
// Get environment with Tor proxy if needed
const gitEnv = this.getGitEnvForUrl(url);
// Configure git proxy for this remote if it's a .onion address
if (shouldUseTor(url)) {
const proxy = getTorProxy();
if (proxy) {
// Set git config for this specific URL pattern
try {
await execAsync(`cd "${repoPath}" && git config --local http.${url}.proxy socks5://${proxy.host}:${proxy.port}`, { env: gitEnv });
} catch {
// Config might fail, continue anyway
}
}
}
// Fetch from remote with appropriate environment
await execAsync(`cd "${repoPath}" && git fetch ${remoteName} --all`, { env: gitEnv });
// Update all branches
await execAsync(`cd "${repoPath}" && git remote set-head ${remoteName} -a`);
await execAsync(`cd "${repoPath}" && git remote set-head ${remoteName} -a`, { env: gitEnv });
} catch (error) {
logger.error({ error, url, repoPath }, 'Failed to sync from remote');
// Continue with other remotes
@ -134,8 +186,26 @@ export class RepoManager { @@ -134,8 +186,26 @@ export class RepoManager {
try {
const remoteName = `remote-${remoteUrls.indexOf(url)}`;
await execAsync(`cd "${repoPath}" && git remote add ${remoteName} "${url}" || true`);
await execAsync(`cd "${repoPath}" && git push ${remoteName} --all --force`);
await execAsync(`cd "${repoPath}" && git push ${remoteName} --tags --force`);
// Get environment with Tor proxy if needed
const gitEnv = this.getGitEnvForUrl(url);
// Configure git proxy for this remote if it's a .onion address
if (shouldUseTor(url)) {
const proxy = getTorProxy();
if (proxy) {
// Set git config for this specific URL pattern
try {
await execAsync(`cd "${repoPath}" && git config --local http.${url}.proxy socks5://${proxy.host}:${proxy.port}`, { env: gitEnv });
} catch {
// Config might fail, continue anyway
}
}
}
// Push to remote with appropriate environment
await execAsync(`cd "${repoPath}" && git push ${remoteName} --all --force`, { env: gitEnv });
await execAsync(`cd "${repoPath}" && git push ${remoteName} --tags --force`, { env: gitEnv });
} catch (error) {
logger.error({ error, url, repoPath }, 'Failed to sync to remote');
// Continue with other remotes

147
src/lib/services/nostr/nostr-client.ts

@ -5,6 +5,7 @@ @@ -5,6 +5,7 @@
import type { NostrEvent, NostrFilter } from '../../types/nostr.js';
import logger from '../logger.js';
import { isNIP07Available, getPublicKeyWithNIP07, signEventWithNIP07 } from './nip07-signer.js';
import { shouldUseTor, getTorProxy } from '../../utils/tor.js';
// Polyfill WebSocket for Node.js environments (lazy initialization)
// Note: The 'module' import warning in browser builds is expected and harmless.
@ -55,6 +56,72 @@ if (typeof process !== 'undefined' && process.versions?.node && typeof window == @@ -55,6 +56,72 @@ if (typeof process !== 'undefined' && process.versions?.node && typeof window ==
});
}
/**
* Create a WebSocket connection, optionally through Tor SOCKS proxy
*/
async function createWebSocketWithTor(url: string): Promise<WebSocket> {
await initializeWebSocketPolyfill();
// Check if we need Tor
if (!shouldUseTor(url)) {
return new WebSocket(url);
}
// Only use Tor in Node.js environment
if (typeof process === 'undefined' || !process.versions?.node || typeof window !== 'undefined') {
// Browser environment - can't use SOCKS proxy directly
// Fall back to regular WebSocket (will fail for .onion in browser)
logger.warn({ url }, 'Tor support not available in browser. .onion addresses may not work.');
return new WebSocket(url);
}
const proxy = getTorProxy();
if (!proxy) {
logger.warn({ url }, 'Tor proxy not configured. Cannot connect to .onion address.');
return new WebSocket(url);
}
try {
// Dynamic import for SOCKS support
const { SocksClient } = await import('socks');
const { WebSocket: WS } = await import('ws');
// Parse the WebSocket URL
const wsUrl = new URL(url);
const host = wsUrl.hostname;
const port = wsUrl.port ? parseInt(wsUrl.port, 10) : (wsUrl.protocol === 'wss:' ? 443 : 80);
// Create SOCKS connection
const socksOptions = {
proxy: {
host: proxy.host,
port: proxy.port,
type: 5 // SOCKS5
},
command: 'connect',
destination: {
host,
port
}
};
const info = await SocksClient.createConnection(socksOptions);
// Create WebSocket over the SOCKS connection
const ws = new WS(url, {
socket: info.socket,
// For wss://, we need to handle TLS
rejectUnauthorized: false // .onion addresses use self-signed certs
});
return ws as any as WebSocket;
} catch (error) {
logger.error({ error, url, proxy }, 'Failed to create WebSocket through Tor');
// Fall back to regular connection (will likely fail for .onion)
return new WebSocket(url);
}
}
export class NostrClient {
private relays: string[] = [];
private authenticatedRelays: Set<string> = new Set();
@ -187,22 +254,26 @@ export class NostrClient { @@ -187,22 +254,26 @@ export class NostrClient {
let authPromise: Promise<boolean> | null = null;
try {
ws = new WebSocket(relay);
} catch (error) {
// Create WebSocket connection (with Tor support if needed)
createWebSocketWithTor(relay).then(websocket => {
ws = websocket;
setupWebSocketHandlers();
}).catch(error => {
// Connection failed immediately
resolveOnce([]);
return;
}
// Connection timeout - if we can't connect within 3 seconds, give up
connectionTimeoutId = setTimeout(() => {
if (!resolved && ws && ws.readyState !== WebSocket.OPEN) {
resolveOnce([]);
}
}, 3000);
});
ws.onopen = () => {
function setupWebSocketHandlers() {
if (!ws) return;
// Connection timeout - if we can't connect within 3 seconds, give up
connectionTimeoutId = setTimeout(() => {
if (!resolved && ws && ws.readyState !== WebSocket.OPEN) {
resolveOnce([]);
}
}, 3000);
ws.onopen = () => {
if (connectionTimeoutId) {
clearTimeout(connectionTimeoutId);
connectionTimeoutId = null;
@ -271,10 +342,11 @@ export class NostrClient { @@ -271,10 +342,11 @@ export class NostrClient {
}
};
// Overall timeout - resolve with what we have after 8 seconds
timeoutId = setTimeout(() => {
resolveOnce(events);
}, 8000);
// Overall timeout - resolve with what we have after 8 seconds
timeoutId = setTimeout(() => {
resolveOnce(events);
}, 8000);
}
});
}
@ -342,23 +414,27 @@ export class NostrClient { @@ -342,23 +414,27 @@ export class NostrClient {
}
};
try {
ws = new WebSocket(relay);
} catch (error) {
rejectOnce(new Error(`Failed to create WebSocket connection to ${relay}`));
return;
}
// Connection timeout - if we can't connect within 3 seconds, reject
connectionTimeoutId = setTimeout(() => {
if (!resolved && ws && ws.readyState !== WebSocket.OPEN) {
rejectOnce(new Error(`Connection timeout for ${relay}`));
}
}, 3000);
let authPromise: Promise<boolean> | null = null;
ws.onopen = () => {
// Create WebSocket connection (with Tor support if needed)
createWebSocketWithTor(relay).then(websocket => {
ws = websocket;
setupWebSocketHandlers();
}).catch(error => {
rejectOnce(new Error(`Failed to create WebSocket connection to ${relay}: ${error}`));
});
function setupWebSocketHandlers() {
if (!ws) return;
// Connection timeout - if we can't connect within 3 seconds, reject
connectionTimeoutId = setTimeout(() => {
if (!resolved && ws && ws.readyState !== WebSocket.OPEN) {
rejectOnce(new Error(`Connection timeout for ${relay}`));
}
}, 3000);
ws.onopen = () => {
if (connectionTimeoutId) {
clearTimeout(connectionTimeoutId);
connectionTimeoutId = null;
@ -432,9 +508,10 @@ export class NostrClient { @@ -432,9 +508,10 @@ export class NostrClient {
}
};
timeoutId = setTimeout(() => {
rejectOnce(new Error('Publish timeout'));
}, 10000);
timeoutId = setTimeout(() => {
rejectOnce(new Error('Publish timeout'));
}, 10000);
}
});
}
}

83
src/lib/services/tor/hidden-service.ts

@ -0,0 +1,83 @@ @@ -0,0 +1,83 @@
/**
* Tor Hidden Service management
* Detects and provides .onion addresses for the server
*/
import { readFile, access } from 'fs/promises';
import { constants } from 'fs';
import { join } from 'path';
import logger from '../logger.js';
import { TOR_ENABLED } from '../../config.js';
/**
* Common locations for Tor hidden service hostname files
*/
const TOR_HOSTNAME_PATHS = [
'/var/lib/tor/hidden_service/hostname',
'/var/lib/tor/gitrepublic/hostname',
'/usr/local/var/lib/tor/hidden_service/hostname',
'/home/.tor/hidden_service/hostname',
process.env.TOR_HOSTNAME_FILE || ''
].filter(Boolean);
/**
* Get the Tor hidden service .onion address
* Returns null if Tor is not enabled or .onion address cannot be found
*/
export async function getTorOnionAddress(): Promise<string | null> {
if (!TOR_ENABLED) {
return null;
}
// First, check if explicitly set via environment variable
if (typeof process !== 'undefined' && process.env?.TOR_ONION_ADDRESS) {
const onion = process.env.TOR_ONION_ADDRESS.trim();
if (onion.endsWith('.onion')) {
logger.info({ onion }, 'Using Tor .onion address from environment variable');
return onion;
}
}
// Try to read from Tor hidden service hostname file
for (const hostnamePath of TOR_HOSTNAME_PATHS) {
if (!hostnamePath) continue;
try {
await access(hostnamePath, constants.R_OK);
const hostname = await readFile(hostnamePath, 'utf-8');
const onion = hostname.trim().split('\n')[0].trim();
if (onion.endsWith('.onion')) {
logger.info({ onion, path: hostnamePath }, 'Found Tor .onion address from hostname file');
return onion;
}
} catch {
// File doesn't exist or can't be read, try next path
continue;
}
}
logger.warn('Tor is enabled but .onion address not found. Set TOR_ONION_ADDRESS env var or configure Tor hidden service.');
return null;
}
/**
* Get the full git URL with Tor .onion address for a repository
*/
export async function getTorGitUrl(npub: string, repoName: string): Promise<string | null> {
const onion = await getTorOnionAddress();
if (!onion) {
return null;
}
// Use HTTP for .onion addresses (HTTPS doesn't work with .onion)
return `http://${onion}/${npub}/${repoName}.git`;
}
/**
* Check if Tor hidden service is available
*/
export async function isTorHiddenServiceAvailable(): Promise<boolean> {
const onion = await getTorOnionAddress();
return onion !== null;
}

38
src/lib/utils/tor.ts

@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
/**
* Tor utility functions for detecting and handling .onion addresses
*/
import { isOnionAddress, TOR_ENABLED, parseTorProxy } from '../config.js';
/**
* Check if a URL should use Tor proxy
*/
export function shouldUseTor(url: string): boolean {
return TOR_ENABLED && isOnionAddress(url);
}
/**
* Get Tor SOCKS proxy configuration
*/
export function getTorProxy(): { host: string; port: number } | null {
return parseTorProxy();
}
/**
* Format git URL with Tor proxy configuration
* Returns the original URL if Tor is not needed or not available
*/
export function formatGitUrlWithTor(url: string): string {
if (!shouldUseTor(url)) {
return url;
}
const proxy = getTorProxy();
if (!proxy) {
return url;
}
// Git can use Tor via GIT_PROXY_COMMAND or http.proxy
// For now, return the original URL - git will be configured separately
return url;
}

13
src/routes/api/repos/[npub]/[repo]/settings/+server.ts

@ -174,11 +174,22 @@ export const POST: RequestHandler = async ({ params, request }) => { @@ -174,11 +174,22 @@ export const POST: RequestHandler = async ({ params, request }) => {
const protocol = gitDomain.startsWith('localhost') ? 'http' : 'https';
const gitUrl = `${protocol}://${gitDomain}/${npub}/${repo}.git`;
// Get Tor .onion URL if available
const { getTorGitUrl } = await import('$lib/services/tor/hidden-service.js');
const torOnionUrl = await getTorGitUrl(npub, repo);
// Build clone URLs - include regular domain and Tor .onion if available
const cloneUrlList = [
gitUrl,
...(torOnionUrl ? [torOnionUrl] : []),
...(cloneUrls || []).filter((url: string) => url && !url.includes(gitDomain) && !url.includes('.onion'))
];
const tags: string[][] = [
['d', repo],
['name', name || repo],
...(description ? [['description', description]] : []),
['clone', gitUrl, ...(cloneUrls || []).filter((url: string) => url && !url.includes(gitDomain))],
['clone', ...cloneUrlList],
['relays', ...DEFAULT_NOSTR_RELAYS],
...(isPrivate ? [['private', 'true']] : []),
...(maintainers || []).map((m: string) => ['maintainers', m])

12
src/routes/api/tor/onion/+server.ts

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
/**
* API endpoint to get the Tor .onion address for the server
*/
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { getTorOnionAddress } from '$lib/services/tor/hidden-service.js';
export const GET: RequestHandler = async () => {
const onion = await getTorOnionAddress();
return json({ onion, available: onion !== null });
};

19
src/routes/signup/+page.svelte

@ -882,10 +882,25 @@ @@ -882,10 +882,25 @@
const protocol = gitDomain.startsWith('localhost') ? 'http' : 'https';
const gitUrl = `${protocol}://${gitDomain}/${npub}/${dTag}.git`;
// Build clone URLs - always include our domain
// Try to get Tor .onion address and add it to clone URLs
let torOnionUrl: string | null = null;
try {
const torResponse = await fetch('/api/tor/onion');
if (torResponse.ok) {
const torData = await torResponse.json();
if (torData.available && torData.onion) {
torOnionUrl = `http://${torData.onion}/${npub}/${dTag}.git`;
}
}
} catch {
// Tor not available, continue without it
}
// Build clone URLs - always include our domain, and Tor .onion if available
const allCloneUrls = [
gitUrl,
...cloneUrls.filter(url => url.trim() && !url.includes(gitDomain))
...(torOnionUrl ? [torOnionUrl] : []), // Add Tor .onion URL if available
...cloneUrls.filter(url => url.trim() && !url.includes(gitDomain) && !url.includes('.onion'))
];
// Build web URLs

Loading…
Cancel
Save