Skip to content
This repository was archived by the owner on Feb 23, 2021. It is now read-only.

Commit 10f8c69

Browse files
Merge pull request #421 from lightninglabs/lightning-uri
Lightning uri
2 parents 691b3b0 + aa7ee63 commit 10f8c69

File tree

8 files changed

+174
-25
lines changed

8 files changed

+174
-25
lines changed

public/electron.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,6 @@ app.on('quit', () => {
195195

196196
app.setAsDefaultProtocolClient(PREFIX_NAME);
197197
app.on('open-url', (event, url) => {
198-
// event.preventDefault();
199-
Logger.info(`open-url# ${url}`);
200198
win && win.webContents.send('open-url', url);
201199
});
202200

src/action/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ store.init();
2525

2626
export const db = new AppStorage(store, AsyncStorage);
2727
export const log = new LogAction(store, ipcRenderer);
28-
export const nav = new NavAction(store, ipcRenderer);
28+
export const nav = new NavAction(store);
2929
export const grpc = new GrpcAction(store, ipcRenderer);
3030
export const notify = new NotificationAction(store, nav);
3131
export const wallet = new WalletAction(store, grpc, db, nav, notify);
@@ -43,6 +43,8 @@ export const invoice = new InvoiceAction(
4343
export const payment = new PaymentAction(store, grpc, transaction, nav, notify);
4444
export const setting = new SettingAction(store, wallet, db);
4545

46+
payment.listenForUrl(ipcRenderer);
47+
4648
//
4749
// Init actions
4850
//

src/action/nav.js

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
1-
import * as log from './log';
2-
31
class NavAction {
4-
constructor(store, ipcRenderer) {
2+
constructor(store) {
53
this._store = store;
6-
ipcRenderer.on('open-url', (event, arg) => {
7-
// TODO: Go to route
8-
log.info('open-url', arg);
9-
});
104
}
115

126
goLoader() {

src/action/payment.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { PREFIX_URI } from '../config';
2-
import { toSatoshis, toAmount, parseSat } from '../helper';
2+
import {
3+
toSatoshis,
4+
toAmount,
5+
parseSat,
6+
isLnUri,
7+
isAddress,
8+
nap,
9+
} from '../helper';
310
import * as log from './log';
411

512
class PaymentAction {
@@ -11,6 +18,21 @@ class PaymentAction {
1118
this._notification = notification;
1219
}
1320

21+
listenForUrl(ipcRenderer) {
22+
ipcRenderer.on('open-url', async (event, url) => {
23+
log.info('open-url', url);
24+
if (!isLnUri(url)) {
25+
return;
26+
}
27+
while (!this._store.lndReady) {
28+
this._tOpenUri = await nap(100);
29+
}
30+
this.init();
31+
this.setAddress({ address: url.replace(PREFIX_URI, '') });
32+
this.checkType();
33+
});
34+
}
35+
1436
init() {
1537
this._store.payment.address = '';
1638
this._store.payment.amount = '';
@@ -33,8 +55,10 @@ class PaymentAction {
3355
}
3456
if (await this.decodeInvoice({ invoice: this._store.payment.address })) {
3557
this._nav.goPayLightningConfirm();
36-
} else {
58+
} else if (isAddress(this._store.payment.address)) {
3759
this._nav.goPayBitcoin();
60+
} else {
61+
this._notification.display({ msg: 'Invalid invoice or address' });
3862
}
3963
}
4064

src/helper.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,26 @@ export const reverse = src => {
180180
return buffer;
181181
};
182182

183+
/**
184+
* Basic uri validation before rendering. More thorough matching
185+
* is done by lnd. This is just to mitigates XSS.
186+
* @param {string} str The uri to validate
187+
* @return {boolean} If the uri is valid
188+
*/
189+
export const isLnUri = str => {
190+
return /^lightning:ln[a-zA-Z0-9]*$/.test(str);
191+
};
192+
193+
/**
194+
* Basic bitcoin address validation. More thorough matching is
195+
* done by lnd. This is just to mitigate XSS.
196+
* @param {string} str The address to validate
197+
* @return {boolean} If the uri is valid
198+
*/
199+
export const isAddress = str => {
200+
return /^[a-km-zA-HJ-NP-Z0-9]{26,35}$/.test(str);
201+
};
202+
183203
/**
184204
* Check if the HTTP status code signals is successful
185205
* @param {Object} response The fetch api's response object

test/unit/action/nav.spec.js

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,19 @@ import NavAction from '../../../src/action/nav';
55
describe('Action Nav Unit Tests', () => {
66
let store;
77
let sandbox;
8-
let ipcRenderer;
98
let nav;
109

1110
beforeEach(() => {
1211
sandbox = sinon.createSandbox({});
1312
sandbox.stub(log);
14-
ipcRenderer = {
15-
send: sinon.stub(),
16-
on: sinon.stub().yields('some-event', 'some-arg'),
17-
};
1813
store = new Store();
19-
nav = new NavAction(store, ipcRenderer);
14+
nav = new NavAction(store);
2015
});
2116

2217
afterEach(() => {
2318
sandbox.restore();
2419
});
2520

26-
describe('constructor()', () => {
27-
it('should listen to open-url event', () => {
28-
expect(nav._store, 'to be ok');
29-
expect(ipcRenderer.on, 'was called with', 'open-url');
30-
});
31-
});
32-
3321
describe('goLoader()', () => {
3422
it('should set correct route', () => {
3523
nav.goLoader();

test/unit/action/payment.spec.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import { EventEmitter } from 'events';
12
import { Store } from '../../../src/store';
23
import GrpcAction from '../../../src/action/grpc';
34
import PaymentAction from '../../../src/action/payment';
45
import TransactionAction from '../../../src/action/transaction';
56
import NotificationAction from '../../../src/action/notification';
67
import NavAction from '../../../src/action/nav';
78
import * as logger from '../../../src/action/log';
9+
import { nap } from '../../../src/helper';
810

911
describe('Action Payments Unit Tests', () => {
1012
let store;
@@ -28,9 +30,48 @@ describe('Action Payments Unit Tests', () => {
2830
});
2931

3032
afterEach(() => {
33+
clearTimeout(payment.tOpenUri);
3134
sandbox.restore();
3235
});
3336

37+
describe('listenForUrl()', () => {
38+
let ipcRendererStub;
39+
40+
beforeEach(() => {
41+
ipcRendererStub = new EventEmitter();
42+
payment.listenForUrl(ipcRendererStub);
43+
sandbox.stub(payment, 'init');
44+
sandbox.stub(payment, 'checkType');
45+
});
46+
47+
it('should not navigate to payment view for invalid uri', () => {
48+
const uri = 'invalid-uri';
49+
ipcRendererStub.emit('open-url', 'some-event', uri);
50+
expect(payment.init, 'was not called');
51+
});
52+
53+
it('should navigate to payment view for valid uri and lndReady', () => {
54+
store.lndReady = true;
55+
const uri = 'lightning:lntb100n1pdn2e0app';
56+
ipcRendererStub.emit('open-url', 'some-event', uri);
57+
expect(payment.init, 'was called once');
58+
expect(store.payment.address, 'to equal', 'lntb100n1pdn2e0app');
59+
expect(payment.checkType, 'was called once');
60+
});
61+
62+
it('should wait for lndReady', async () => {
63+
store.lndReady = false;
64+
const uri = 'lightning:lntb100n1pdn2e0app';
65+
ipcRendererStub.emit('open-url', 'some-event', uri);
66+
expect(payment.init, 'was not called');
67+
store.lndReady = true;
68+
await nap(300);
69+
expect(payment.init, 'was called once');
70+
expect(store.payment.address, 'to equal', 'lntb100n1pdn2e0app');
71+
expect(payment.checkType, 'was called once');
72+
});
73+
});
74+
3475
describe('init()', () => {
3576
it('should clear attributes and navigate to payment view', () => {
3677
store.payment.address = 'foo';
@@ -58,6 +99,41 @@ describe('Action Payments Unit Tests', () => {
5899
});
59100
});
60101

102+
describe('checkType()', () => {
103+
beforeEach(() => {
104+
sandbox.stub(payment, 'decodeInvoice');
105+
});
106+
107+
it('should notify if address is empty', async () => {
108+
payment.decodeInvoice.resolves(true);
109+
await payment.checkType();
110+
expect(notification.display, 'was called once');
111+
expect(payment.decodeInvoice, 'was not called');
112+
});
113+
114+
it('should decode successfully', async () => {
115+
store.payment.address = 'some-address';
116+
payment.decodeInvoice.resolves(true);
117+
await payment.checkType();
118+
expect(nav.goPayLightningConfirm, 'was called once');
119+
});
120+
121+
it('should notify if not bitcoin address', async () => {
122+
store.payment.address = 'some-address';
123+
payment.decodeInvoice.resolves(false);
124+
await payment.checkType();
125+
expect(nav.goPayBitcoin, 'was not called');
126+
expect(notification.display, 'was called once');
127+
});
128+
129+
it('should navigate to bitcoin for valid address', async () => {
130+
store.payment.address = 'rfu4i1Mo2NF7TQsN9bMVLFSojSzcyQCEH5';
131+
payment.decodeInvoice.resolves(false);
132+
await payment.checkType();
133+
expect(nav.goPayBitcoin, 'was called once');
134+
});
135+
});
136+
61137
describe('decodeInvoice()', () => {
62138
it('should decode successfully', async () => {
63139
grpc.sendCommand.withArgs('decodePayReq').resolves({

test/unit/helper.spec.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,53 @@ describe('Helpers Unit Tests', () => {
508508
});
509509
});
510510

511+
describe('isLnUri()', () => {
512+
it('should accept lightning uri', () => {
513+
const uri =
514+
'lightning:lntb1500n1pdn2e0app5wlyxzspccpfvqmrtfr8p487xcch4hxtu2u0qzcke6mzpv222w8usdpa2fjkzep6ypxx2ap8wvs8qmrp0ysxzgrvd9nksarwd9hxwgrwv468wmmjdvsxwcqzysmr9jxv06zx53cyqa0sqntehy5tyrqu064xvw00qjep5f9gw57qcqp6qnpqyuprh90aqzfyf9ypq8uth7qte5ecjq0fng3y47mywwkfqq3megny';
515+
expect(helpers.isLnUri(uri), 'to be', true);
516+
});
517+
518+
it('should reject bitcoin uri', () => {
519+
const uri = 'bitcoin:rfu4i1Mo2NF7TQsN9bMVLFSojSzcyQCEH5';
520+
expect(helpers.isLnUri(uri), 'to be', false);
521+
});
522+
523+
it('should reject bitcoin address', () => {
524+
const uri = 'rfu4i1Mo2NF7TQsN9bMVLFSojSzcyQCEH5';
525+
expect(helpers.isLnUri(uri), 'to be', false);
526+
});
527+
528+
it('should reject lightning invoice', () => {
529+
const uri =
530+
'lntb1500n1pdn2e0app5wlyxzspccpfvqmrtfr8p487xcch4hxtu2u0qzcke6mzpv222w8usdpa2fjkzep6ypxx2ap8wvs8qmrp0ysxzgrvd9nksarwd9hxwgrwv468wmmjdvsxwcqzysmr9jxv06zx53cyqa0sqntehy5tyrqu064xvw00qjep5f9gw57qcqp6qnpqyuprh90aqzfyf9ypq8uth7qte5ecjq0fng3y47mywwkfqq3megny';
531+
expect(helpers.isLnUri(uri), 'to be', false);
532+
});
533+
534+
it('should mitigate xss', () => {
535+
const uri =
536+
'lightning:lntb1500n1<script>alert("XSS")</script>p487xcch4hxtu2u0qzcke6mzpv222w8usdpa2fjkzep6ypxx2ap8wvs8qmrp0ysxzgrvd9nksarwd9hxwgrwv468wmmjdvsxwcqzysmr9jxv06zx53cyqa0sqntehy5tyrqu064xvw00qjep5f9gw57qcqp6qnpqyuprh90aqzfyf9ypq8uth7qte5ecjq0fng3y47mywwkfqq3megny';
537+
expect(helpers.isLnUri(uri), 'to be', false);
538+
});
539+
});
540+
541+
describe('isAddress()', () => {
542+
it('should accept bitcoin uri', () => {
543+
const address = 'rfu4i1Mo2NF7TQsN9bMVLFSojSzcyQCEH5';
544+
expect(helpers.isAddress(address), 'to be', true);
545+
});
546+
547+
it('should reject invalid bitcoin uri', () => {
548+
const address = '/INVALID/rfu4i1Mo2NF7TQsN9bMVLFSoj';
549+
expect(helpers.isAddress(address), 'to be', false);
550+
});
551+
552+
it('should mitigate xss', () => {
553+
const address = 'rfu<script>alert("XSS")</script>';
554+
expect(helpers.isAddress(address), 'to be', false);
555+
});
556+
});
557+
511558
describe('checkHttpStatus()', () => {
512559
it('should throw error for 500', () => {
513560
const response = { status: 500, statusText: 'Boom!' };

0 commit comments

Comments
 (0)