diff --git a/Dockerfile b/Dockerfile index c79fa23..f795417 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,7 @@ FROM node:18-alpine3.16 as build WORKDIR /build +RUN apk add --no-cache --update git COPY ["package.json", "package-lock.json", "./"] @@ -26,6 +27,8 @@ ENV DB_USER=nostr-ts-relay ENV DB_PASSWORD=nostr-ts-relay WORKDIR /app +RUN apk add --no-cache --update git +RUN mkdir /app/.nostr && chown 1000:1000 /app/.nostr COPY --from=build /build/dist . diff --git a/docker-compose.yml b/docker-compose.yml index 8da67c1..4d339b0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,10 @@ services: REDIS_PORT: 6379 REDIS_USER: default REDIS_PASSWORD: nostr_ts_relay + TOR_HOST: tor_proxy + TOR_CONTROL_PORT: 9051 + TOR_PASSWORD: nostr_ts_relay + HIDDEN_SERVICE_PORT: 80 # Enable DEBUG for troubleshooting. Examples: # DEBUG: "worker:*" # DEBUG: "knex:query" @@ -31,13 +35,14 @@ services: condition: service_healthy migrations: condition: service_completed_successfully + tor_proxy: + condition: service_healthy restart: on-failure networks: default: ipv4_address: 10.10.10.2 db: image: postgres - container_name: db environment: POSTGRES_DB: nostr_ts_relay POSTGRES_USER: nostr_ts_relay @@ -62,7 +67,6 @@ services: start_period: 360s cache: image: redis:7.0.5-alpine3.16 - container_name: cache volumes: - cache:/data command: redis-server --save 20 1 --loglevel warning --requirepass nostr_ts_relay @@ -77,7 +81,6 @@ services: retries: 5 migrations: image: node:18-alpine3.16 - container_name: migrations environment: DB_HOST: db DB_PORT: 5432 @@ -97,7 +100,19 @@ services: networks: default: ipv4_address: 10.10.10.254 - + tor_proxy: + image: dperson/torproxy + restart: on-failure + environment: + PASSWORD: nostr_ts_relay + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + networks: + default: + ipv4_address: 10.10.10.5 networks: default: name: nostr-ts-relay diff --git a/package-lock.json b/package-lock.json index dc0f363..89e17ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "pg-query-stream": "4.2.4", "ramda": "0.28.0", "redis": "4.5.1", + "tor-control-ts": "^1.0.0", "ws": "8.11.0" }, "devDependencies": { @@ -11806,6 +11807,11 @@ "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", "dev": true }, + "node_modules/tor-control-ts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tor-control-ts/-/tor-control-ts-1.0.0.tgz", + "integrity": "sha512-uV+swAIQuH0QP+SJcQwlj2xrv0XqKa9V1HQOkU+NONR7/8+JM/4uIxzDJDsVtiK6cNq+x5Nt6deh3nx16XK1Yg==" + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -21408,6 +21414,11 @@ "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", "dev": true }, + "tor-control-ts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tor-control-ts/-/tor-control-ts-1.0.0.tgz", + "integrity": "sha512-uV+swAIQuH0QP+SJcQwlj2xrv0XqKa9V1HQOkU+NONR7/8+JM/4uIxzDJDsVtiK6cNq+x5Nt6deh3nx16XK1Yg==" + }, "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", diff --git a/package.json b/package.json index e5d0421..45f2ecc 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,7 @@ "pg-query-stream": "4.2.4", "ramda": "0.28.0", "redis": "4.5.1", + "tor-control-ts": "^1.0.0", "ws": "8.11.0" }, "config": { diff --git a/src/app/app.ts b/src/app/app.ts index 23d0631..c1aa430 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -1,6 +1,7 @@ import { Cluster, Worker } from 'cluster' -import { cpus } from 'os' +import { cpus, hostname } from 'os' +import { addOnion } from '../tor/client' import { createLogger } from '../factories/logger-factory' import { IRunnable } from '../@types/base' import { ISettings } from '../@types/settings' @@ -39,6 +40,9 @@ export class App implements IRunnable { ░ ░ ░ ░ ░ ░ ▒ ░ ░ ░ ░ ░░ ░ ░ ░ ▒ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░`) const width = 74 + const torHiddenServicePort = process.env.HIDDEN_SERVICE_PORT ? Number(process.env.HIDDEN_SERVICE_PORT) : 80 + const port = process.env.RELAY_PORT ? Number(process.env.RELAY_PORT) : 8008 + const logCentered = (input: string, width: number) => { const start = (width >> 1) - (input.length >> 1) console.log(' '.repeat(start), input) @@ -58,6 +62,13 @@ export class App implements IRunnable { logCentered(`${workerCount} workers started`, width) debug('settings: %O', this.settingsFactory()) + + const host = `${hostname()}:${port}}` + addOnion(torHiddenServicePort, host).then(value=>{ + debug('tor hidden service address: %s:%d', value, torHiddenServicePort) + }, (error) => { + console.error('Unable to add Tor hidden service. Skipping.', error) + }) } private onClusterMessage(source: Worker, message: Serializable) { diff --git a/src/tor/client.ts b/src/tor/client.ts new file mode 100644 index 0000000..8fd46d9 --- /dev/null +++ b/src/tor/client.ts @@ -0,0 +1,83 @@ +import {Tor} from "tor-control-ts" +import { createLogger } from '../factories/logger-factory' +import {readFile,writeFile} from 'fs/promises'; +import { homedir } from "os"; +import { join } from "path"; + + +interface torParams{ + host:string; + port:number; + password:string; +} + +const debug = createLogger('tor-client') + +const getPrivKeyFile = ()=>{ + return join(process.env.NOSTR_CONFIG_DIR ?? join(homedir(), '.nostr'),"v3_onion_private_key"); +} + +const createTorConfig = ():torParams => { + return { + host:process.env.TOR_HOST, + port:Number(process.env.TOR_CONTROL_PORT), + password:process.env.TOR_PASSWORD + }; +} + +let client:any = null; + +export const getTorClient = async () => { + if (!client) { + const config = createTorConfig(); + debug('config: %o', config); + //client = knex(config) + if(config.port){ + debug('connecting'); + client = new Tor(config); + await client.connect(); + debug('connected to tor'); + } + + } + + return client +} +export const addOnion = async (port:number,host?:string):Promise=>{ + let privateKey = null; + + try { + + let data = await readFile(getPrivKeyFile(),{ + encoding:"utf-8" + }); + if(data && data.length){ + privateKey = data; + } + debug('privateKey: %o', privateKey); + } catch (error) { + debug('addOnion catch: %o', error); + } + + try { + await getTorClient(); + if(client){ + let hs = await client.addOnion(port,host,privateKey); + if(hs && hs.PrivateKey){ + await writeFile(getPrivKeyFile(),hs.PrivateKey,{ + encoding:"utf-8" + }); + } + + debug('hs: %o', hs); + debug('hidden service: ', hs.ServiceID+":"+port); + return hs.ServiceID; + }else{ + return null; + } + } catch (error) { + debug('addOnion catch: %o', error); + return null; + } + +} \ No newline at end of file