From 22b84ee007ae53fbfdb9e0f2543b27f29da7cf03 Mon Sep 17 00:00:00 2001 From: Aaron Date: Wed, 22 Mar 2023 07:08:45 +1100 Subject: [PATCH] 0.1.0-0.3.0 - Feature: TCP Socket (#24) * Add tcpSocket option for direct connections * Dynamically import socket wrapper * Increment version * Update README --- README.md | 4 +++- package.json | 3 ++- src/index.ts | 25 +++++++++++++++++------ src/noise-state.ts | 6 +++--- src/socket-wrapper.ts | 47 +++++++++++++++++++++++++++++++++++++++++++ src/types.ts | 3 +++ 6 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 src/socket-wrapper.ts diff --git a/README.md b/README.md index d25e17e..2490022 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Talk to Lightning nodes from the Browser and NodeJS apps. ## Features -- Connect to a lightning node via a WebSocket connection. +- Connect to a lightning node via a WebSocket or TCP Socket connection. - Works in the Browser and Node without any polyfilling. - Initialise with a session secret to have a persistent node public key for the browser. - Control a Core Lightning node via [Commando](https://lightning.readthedocs.io/lightning-commando.7.html) RPC calls. @@ -84,6 +84,8 @@ type LnWebSocketOptions = { * When connecting directly to a node and not using a proxy, the protocol to use. Defaults to 'wss://' */ wsProtocol?: 'ws:' | 'wss:' + /**In Nodejs or React Native you can connect directly via a TCP socket */ + tcpSocket?: TCPSocket /** * 32 byte hex encoded private key to be used as the local node secret. * Use this to ensure a consistent local node identity across connection sessions diff --git a/package.json b/package.json index 44a8a6b..8865879 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lnmessage", - "version": "0.1.0-0.1.0", + "version": "0.1.0-0.3.0", "description": "Talk to Lightning nodes from your browser", "main": "dist/index.js", "type": "module", @@ -10,6 +10,7 @@ ], "author": "Aaron Barnard", "license": "MIT", + "repository": "github:aaronbarnardsound/lnmessage", "scripts": { "build": "tsc" }, diff --git a/src/index.ts b/src/index.ts index 7042901..52cb156 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,8 @@ import { CommandoMessage } from './messages/CommandoMessage.js' import { PongMessage } from './messages/PongMessage.js' import { PingMessage } from './messages/PingMessage.js' import type { WebSocket as NodeWebSocket } from 'ws' +import type { Socket as TCPSocket } from 'net' +import type SocketWrapper from './socket-wrapper.js' import { LnWebSocketOptions, @@ -47,7 +49,9 @@ class LnMessage { */ public wsUrl: string /**The WebSocket instance*/ - public socket: WebSocket | NodeWebSocket | null + public socket: WebSocket | NodeWebSocket | null | SocketWrapper + /**TCP socket instance*/ + public tcpSocket?: TCPSocket /** * @deprecated Use connectionStatus$ instead */ @@ -96,7 +100,8 @@ class LnMessage { privateKey, ip, port = 9735, - logger + logger, + tcpSocket } = options this._ls = Buffer.from(privateKey || createRandomPrivateKey(), 'hex') @@ -115,6 +120,7 @@ class LnMessage { this.connected$ = new BehaviorSubject(false) this.connecting = false this.Buffer = Buffer + this.tcpSocket = tcpSocket this._handshakeState = HANDSHAKE_STATE.INITIATOR_INITIATING this._decryptedMsgs$ = new Subject() @@ -159,8 +165,15 @@ class LnMessage { this.connectionStatus$.next('connecting') this._attemptReconnect = attemptReconnect - this.socket = new (typeof globalThis.WebSocket === 'undefined' ? (await import('ws')).default : globalThis.WebSocket)(this.wsUrl) - this.socket.binaryType = 'arraybuffer' + this.socket = this.tcpSocket + ? new (await import('./socket-wrapper.js')).default(this.wsUrl, this.tcpSocket) + : typeof globalThis.WebSocket === 'undefined' + ? new (await import('ws')).default(this.wsUrl) + : new globalThis.WebSocket(this.wsUrl) + + if ((this.socket as WebSocket | NodeWebSocket).binaryType) { + ;(this.socket as WebSocket | NodeWebSocket).binaryType = 'arraybuffer' + } this.socket.onopen = async () => { this._log('info', 'WebSocket is connected') @@ -209,8 +222,8 @@ class LnMessage { ) } - private queueMessage(event: MessageEvent) { - const { data } = event as { data: ArrayBuffer } + private queueMessage(event: { data: ArrayBuffer }) { + const { data } = event const message = Buffer.from(data) const currentData = diff --git a/src/noise-state.ts b/src/noise-state.ts index 3c9dfbb..0f7f7e9 100644 --- a/src/noise-state.ts +++ b/src/noise-state.ts @@ -120,7 +120,7 @@ export class NoiseState { // 4. ck, temp_k1 = HKDF(ck, es) const tempK1 = hkdf(ss, 64, this.ck) this.ck = tempK1.subarray(0, 32) - this.tempK1 = tempK1.subarray(32) + this.tempK1 = Buffer.from(tempK1.subarray(32)) // 5. c = encryptWithAD(temp_k1, 0, h, zero) const c = ccpEncrypt(this.tempK1, Buffer.alloc(12), this.h, Buffer.alloc(0)) @@ -183,7 +183,7 @@ export class NoiseState { // 4. ck, temp_k3 = HKDF(ck, ss) const tempK3 = hkdf(ss, 64, this.ck) this.ck = tempK3.subarray(0, 32) - this.tempK3 = tempK3.subarray(32) + this.tempK3 = Buffer.from(tempK3.subarray(32)) // 5. t = encryptWithAD(temp_k3, 0, h, zero) const t = ccpEncrypt(this.tempK3, Buffer.alloc(12), this.h, Buffer.alloc(0)) // 6. sk, rk = hkdf(ck, zero) @@ -239,7 +239,7 @@ export class NoiseState { // 4. ck, temp_k2 = hkdf(ck, ss) const tempK2 = hkdf(ss, 64, this.ck) this.ck = tempK2.subarray(0, 32) - this.tempK2 = tempK2.subarray(32) + this.tempK2 = Buffer.from(tempK2.subarray(32)) // 5. c = encryptWithAd(temp_k2, 0, h, zero) const c = ccpEncrypt(this.tempK2, Buffer.alloc(12), this.h, Buffer.alloc(0)) // 6. h = sha256(h || c) diff --git a/src/socket-wrapper.ts b/src/socket-wrapper.ts new file mode 100644 index 0000000..adfd840 --- /dev/null +++ b/src/socket-wrapper.ts @@ -0,0 +1,47 @@ +import type { Socket } from 'net' +import type { Buffer } from 'buffer' + +//**Wraps a TCP socket with the WebSocket API */ +class SocketWrapper { + public onopen?: () => void + public onclose?: () => void + public onerror?: (error: { message: string }) => void + public onmessage?: (event: { data: ArrayBuffer }) => void + public send: (message: Buffer) => void + public close: () => void + + constructor(connection: string, socket: Socket) { + socket.on('connect', () => { + this.onopen && this.onopen() + }) + + socket.on('close', () => { + this.onclose && this.onclose() + }) + + socket.on('error', (error) => { + this.onerror && this.onerror(error) + }) + + socket.on('data', (data) => { + this.onmessage && this.onmessage({ data }) + }) + + this.send = (message: Buffer) => { + socket.write(message) + } + + this.close = () => { + socket.removeAllListeners() + socket.destroy() + } + + const url = new URL(connection) + const { host } = url + const [nodeIP, port] = host.split(':') + + socket.connect(parseInt(port), nodeIP) + } +} + +export default SocketWrapper diff --git a/src/types.ts b/src/types.ts index 65f2c22..4745fd0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,5 @@ import type { Buffer } from 'buffer' +import type { Socket as TCPSocket } from 'net' export type LnWebSocketOptions = { /** @@ -25,6 +26,8 @@ export type LnWebSocketOptions = { * When connecting directly to a node, the protocol to use. Defaults to 'wss://' */ wsProtocol?: 'ws:' | 'wss:' + /**In nodejs or react native you can connect directly via a TCP socket */ + tcpSocket?: TCPSocket /** * 32 byte hex encoded private key to be used as the local node secret. * Use this to ensure a consistent local node identity across connection sessions