diff --git a/class/lnurl.js b/class/lnurl.js index c82dd7a51..9a9835373 100644 --- a/class/lnurl.js +++ b/class/lnurl.js @@ -1,7 +1,10 @@ import { bech32 } from 'bech32'; import bolt11 from 'bolt11'; +import { isTorDaemonDisabled } from '../blue_modules/environment'; const CryptoJS = require('crypto-js'); const createHash = require('create-hash'); +const torrific = require('../blue_modules/torrific'); +const ONION_REGEX = /^(http:\/\/[^/:@]+\.onion(?::\d{1,5})?)(\/.*)?$/; // regex for onion URL /** * @see https://github.com/btcontract/lnurl-rfc/blob/master/lnurl-pay.md @@ -32,7 +35,8 @@ export default class Lnurl { if (Lnurl.isLightningAddress(lnurlExample)) { const username = lnurlExample.split('@')[0].trim(); const host = lnurlExample.split('@')[1].trim(); - return `https://${host}/.well-known/lnurlp/${username}`; + const proto = host.match(/\.onion$/) ? 'http' : 'https'; + return `${proto}://${host}/.well-known/lnurlp/${username}`; } else { return false; } @@ -46,7 +50,23 @@ export default class Lnurl { return Lnurl.findlnurl(url) !== null; } + static isOnionUrl(url) { + return Lnurl.parseOnionUrl(url) !== null; + } + + static parseOnionUrl(url) { + const match = url.match(ONION_REGEX); + if (match === null) return null; + const [, baseURI, path] = match; + return [baseURI, path]; + } + async fetchGet(url) { + const parsedOnionUrl = Lnurl.parseOnionUrl(url); + if (parsedOnionUrl) { + return _fetchGetTor(parsedOnionUrl); + } + const resp = await fetch(url, { method: 'GET' }); if (resp.status >= 300) { throw new Error('Bad response from server'); @@ -272,3 +292,28 @@ export default class Lnurl { return !!splitted[0].trim() && !!splitted[1].trim(); } } + +async function _fetchGetTor(parsedOnionUrl) { + const torDaemonDisabled = await isTorDaemonDisabled(); + if (torDaemonDisabled) { + throw new Error('Tor onion url support disabled'); + } + const [baseURI, path] = parsedOnionUrl; + const tor = new torrific.Torsbee({ + baseURI, + }); + const response = await tor.get(path || '/', { + headers: { + 'Access-Control-Allow-Origin': '*', + 'Content-Type': 'application/json', + }, + }); + const json = response.body; + if (typeof json === 'undefined' || response.err) { + throw new Error('Bad response from server: ' + response.err + ' ' + JSON.stringify(response.body)); + } + if (json.status === 'ERROR') { + throw new Error('Reply from server: ' + json.reason); + } + return json; +} diff --git a/tests/unit/lnurl.test.js b/tests/unit/lnurl.test.js index a4bb6a419..b28df5b82 100644 --- a/tests/unit/lnurl.test.js +++ b/tests/unit/lnurl.test.js @@ -42,6 +42,50 @@ describe('LNURL', function () { assert.ok(!Lnurl.isLnurl('bs')); }); + it('can parseOnionUrl()', () => { + const vectors = [ + { + test: 'http://abc.onion/path', + expected: ['http://abc.onion', '/path'], + }, + { + test: 'http://abc.onion:12345/path', + expected: ['http://abc.onion:12345', '/path'], + }, + { + test: 'http://abc.onion/', + expected: ['http://abc.onion', '/'], + }, + { + test: 'http://abc.onion', + expected: ['http://abc.onion', undefined], + }, + { + test: 'https://abc.onion', + expected: null, + }, + { + test: 'http://abc.com', + expected: null, + }, + { + test: 'http://a@bc.onion', + expected: null, + }, + { + test: 'http://a/bc.onion', + expected: null, + }, + { + test: 'http://a:bc.onion', + expected: null, + }, + ]; + for (const { test, expected } of vectors) { + assert.deepStrictEqual(Lnurl.parseOnionUrl(test), expected); + } + }); + it('can callLnurlPayService() and requestBolt11FromLnurlPayService()', async () => { const LN = new Lnurl('LNURL1DP68GURN8GHJ7MRWW3UXYMM59E3XJEMNW4HZU7RE0GHKCMN4WFKZ7URP0YLH2UM9WF5KG0FHXYCNV9G9W58'); @@ -168,6 +212,7 @@ describe('LNURL', function () { describe('lightning address', function () { it('can getUrlFromLnurl()', () => { assert.strictEqual(Lnurl.getUrlFromLnurl('lnaddress@zbd.gg'), 'https://zbd.gg/.well-known/lnurlp/lnaddress'); + assert.strictEqual(Lnurl.getUrlFromLnurl('lnaddress@hidden.onion'), 'http://hidden.onion/.well-known/lnurlp/lnaddress'); }); it('can detect', async () => {