diff --git a/lib/cln.js b/lib/cln.js index 6b06ae99..fbc7d390 100644 --- a/lib/cln.js +++ b/lib/cln.js @@ -1,10 +1,27 @@ import fetch from 'cross-fetch' import https from 'https' import crypto from 'crypto' +import { HttpProxyAgent, HttpsProxyAgent } from '@/lib/proxy' export const createInvoice = async ({ socket, rune, cert, label, description, msats, expiry }) => { - const agent = cert ? new https.Agent({ ca: Buffer.from(cert, 'base64') }) : undefined - const url = 'https://' + socket + '/v1/invoice' + let protocol, agent + const httpsAgentOptions = { ca: cert ? Buffer.from(cert, 'base64') : undefined } + const isOnion = /\.onion(:[0-9]+)?$/.test(socket) + if (isOnion) { + // we support HTTP and HTTPS over Tor + protocol = cert ? 'https:' : 'http:' + // we need to use our Tor proxy to resolve onion addresses + const proxyOptions = { proxy: 'http://127.0.0.1:7050/' } + agent = protocol === 'https:' + ? new HttpsProxyAgent({ ...proxyOptions, ...httpsAgentOptions }) + : new HttpProxyAgent(proxyOptions) + } else { + // we only support HTTPS over clearnet + agent = new https.Agent(httpsAgentOptions) + protocol = 'https:' + } + + const url = `${protocol}//${socket}/v1/invoice` const res = await fetch(url, { method: 'POST', headers: { diff --git a/lib/proxy.js b/lib/proxy.js new file mode 100644 index 00000000..9a900bfb --- /dev/null +++ b/lib/proxy.js @@ -0,0 +1,120 @@ +import http from 'http' +import https from 'https' + +// from https://github.com/delvedor/hpagent + +export class HttpProxyAgent extends http.Agent { + constructor (options) { + const { proxy, proxyRequestOptions, ...opts } = options + super(opts) + this.proxy = typeof proxy === 'string' + ? new URL(proxy) + : proxy + this.proxyRequestOptions = proxyRequestOptions || {} + } + + createConnection (options, callback) { + const requestOptions = { + ...this.proxyRequestOptions, + method: 'CONNECT', + host: this.proxy.hostname, + port: this.proxy.port, + path: `${options.host}:${options.port}`, + setHost: false, + headers: { ...this.proxyRequestOptions.headers, connection: this.keepAlive ? 'keep-alive' : 'close', host: `${options.host}:${options.port}` }, + agent: false, + timeout: options.timeout || 0 + } + + if (this.proxy.username || this.proxy.password) { + const base64 = Buffer.from(`${decodeURIComponent(this.proxy.username || '')}:${decodeURIComponent(this.proxy.password || '')}`).toString('base64') + requestOptions.headers['proxy-authorization'] = `Basic ${base64}` + } + + if (this.proxy.protocol === 'https:') { + requestOptions.servername = this.proxy.hostname + } + + const request = (this.proxy.protocol === 'http:' ? http : https).request(requestOptions) + request.once('connect', (response, socket, head) => { + request.removeAllListeners() + socket.removeAllListeners() + if (response.statusCode === 200) { + callback(null, socket) + } else { + socket.destroy() + callback(new Error(`Bad response: ${response.statusCode}`), null) + } + }) + + request.once('timeout', () => { + request.destroy(new Error('Proxy timeout')) + }) + + request.once('error', err => { + request.removeAllListeners() + callback(err, null) + }) + + request.end() + } +} + +export class HttpsProxyAgent extends https.Agent { + constructor (options) { + const { proxy, proxyRequestOptions, ...opts } = options + super(opts) + this.proxy = typeof proxy === 'string' + ? new URL(proxy) + : proxy + this.proxyRequestOptions = proxyRequestOptions || {} + } + + createConnection (options, callback) { + const requestOptions = { + ...this.proxyRequestOptions, + method: 'CONNECT', + host: this.proxy.hostname, + port: this.proxy.port, + path: `${options.host}:${options.port}`, + setHost: false, + headers: { ...this.proxyRequestOptions.headers, connection: this.keepAlive ? 'keep-alive' : 'close', host: `${options.host}:${options.port}` }, + agent: false, + timeout: options.timeout || 0 + } + + if (this.proxy.username || this.proxy.password) { + const base64 = Buffer.from(`${decodeURIComponent(this.proxy.username || '')}:${decodeURIComponent(this.proxy.password || '')}`).toString('base64') + requestOptions.headers['proxy-authorization'] = `Basic ${base64}` + } + + // Necessary for the TLS check with the proxy to succeed. + if (this.proxy.protocol === 'https:') { + requestOptions.servername = this.proxy.hostname + } + + const request = (this.proxy.protocol === 'http:' ? http : https).request(requestOptions) + request.once('connect', (response, socket, head) => { + request.removeAllListeners() + socket.removeAllListeners() + if (response.statusCode === 200) { + const secureSocket = super.createConnection({ ...options, socket }) + callback(null, secureSocket) + } else { + socket.destroy() + callback(new Error(`Bad response: ${response.statusCode}`), null) + } + }) + + request.once('timeout', () => { + request.destroy(new Error('Proxy timeout')) + }) + + request.once('error', err => { + request.removeAllListeners() + callback(err, null) + }) + + request.end() + } +}