Skip to content

Commit bff4a86

Browse files
committed
experimental-websocket-port: option to create a WebSocket port.
Signed-off-by: Rusty Russell <[email protected]>
1 parent e971a5b commit bff4a86

10 files changed

+142
-33
lines changed

doc/lightning-listconfigs.7.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ On success, an object is returned, containing:
5656
- **experimental-onion-messages** (boolean, optional): `experimental-onion-messages` field from config or cmdline, or default
5757
- **experimental-offers** (boolean, optional): `experimental-offers` field from config or cmdline, or default
5858
- **experimental-shutdown-wrong-funding** (boolean, optional): `experimental-shutdown-wrong-funding` field from config or cmdline, or default
59+
- **experimental-websocket-port** (u16, optional): `experimental-websocket-port` field from config or cmdline, or default
5960
- **rgb** (hex, optional): `rgb` field from config or cmdline, or default (always 6 characters)
6061
- **alias** (string, optional): `alias` field from config or cmdline, or default
6162
- **pid-file** (string, optional): `pid-file` field from config or cmdline, or default
@@ -205,4 +206,4 @@ RESOURCES
205206
---------
206207

207208
Main web site: <https://github.com/ElementsProject/lightning>
208-
[comment]: # ( SHA256STAMP:7bb40fc8fac201b32d9701b02596d0fa59eb14a3baf606439cbf96dc11548ed4)
209+
[comment]: # ( SHA256STAMP:47c067588120e0f9a71206313685cebb2a8c515e9b04b688b202d2772c8f8146)

doc/lightningd-config.5.md

+7
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,13 @@ about whether to add funds or not to a proposed channel is handled
517517
automatically by a plugin that implements the appropriate logic for
518518
your needs. The default behavior is to not contribute funds.
519519

520+
**experimental-websocket-port**
521+
522+
Specifying this enables support for accepting incoming WebSocket
523+
connections on that port, on any IPv4 and IPv6 addresses you listen
524+
to. The normal protocol is expected to be sent over WebSocket binary
525+
frames once the connection is upgraded.
526+
520527
BUGS
521528
----
522529

doc/schemas/listconfigs.schema.json

+4
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@
121121
"type": "boolean",
122122
"description": "`experimental-shutdown-wrong-funding` field from config or cmdline, or default"
123123
},
124+
"experimental-websocket-port": {
125+
"type": "u16",
126+
"description": "`experimental-websocket-port` field from config or cmdline, or default"
127+
},
124128
"rgb": {
125129
"type": "hex",
126130
"description": "`rgb` field from config or cmdline, or default",

lightningd/connect_control.c

+5-2
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,10 @@ int connectd_init(struct lightningd *ld)
350350
int hsmfd;
351351
struct wireaddr_internal *wireaddrs = ld->proposed_wireaddr;
352352
enum addr_listen_announce *listen_announce = ld->proposed_listen_announce;
353-
const char *websocket_helper_path = "";
353+
const char *websocket_helper_path;
354+
355+
websocket_helper_path = subdaemon_path(tmpctx, ld,
356+
"lightning_websocketd");
354357

355358
if (socketpair(AF_LOCAL, SOCK_STREAM, 0, fds) != 0)
356359
fatal("Could not socketpair for connectd<->gossipd");
@@ -384,7 +387,7 @@ int connectd_init(struct lightningd *ld)
384387
ld->config.use_v3_autotor,
385388
ld->config.connection_timeout_secs,
386389
websocket_helper_path,
387-
0);
390+
ld->websocket_port);
388391

389392
subd_req(ld->connectd, ld->connectd, take(msg), -1, 0,
390393
connect_init_done, NULL);

lightningd/lightningd.c

+1
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ static struct lightningd *new_lightningd(const tal_t *ctx)
216216
ld->always_use_proxy = false;
217217
ld->pure_tor_setup = false;
218218
ld->tor_service_password = NULL;
219+
ld->websocket_port = 0;
219220

220221
/*~ This is initialized later, but the plugin loop examines this,
221222
* so set it to NULL explicitly now. */

lightningd/lightningd.h

+3
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,9 @@ struct lightningd {
284284
/* Array of (even) TLV types that we should allow. This is required
285285
* since we otherwise would outright reject them. */
286286
u64 *accept_extra_tlv_types;
287+
288+
/* EXPERIMENTAL: websocket port if non-zero */
289+
u16 websocket_port;
287290
};
288291

289292
/* Turning this on allows a tal allocation to return NULL, rather than aborting.

lightningd/options.c

+25
Original file line numberDiff line numberDiff line change
@@ -850,6 +850,21 @@ static char *opt_set_wumbo(struct lightningd *ld)
850850
return NULL;
851851
}
852852

853+
static char *opt_set_websocket_port(const char *arg, struct lightningd *ld)
854+
{
855+
u32 port COMPILER_WANTS_INIT("9.3.0 -O2");
856+
char *err;
857+
858+
err = opt_set_u32(arg, &port);
859+
if (err)
860+
return err;
861+
862+
ld->websocket_port = port;
863+
if (ld->websocket_port != port)
864+
return tal_fmt(NULL, "'%s' is out of range", arg);
865+
return NULL;
866+
}
867+
853868
static char *opt_set_dual_fund(struct lightningd *ld)
854869
{
855870
/* Dual funding implies anchor outputs */
@@ -1051,6 +1066,11 @@ static void register_opts(struct lightningd *ld)
10511066
"--subdaemon=hsmd:remote_signer "
10521067
"would use a hypothetical remote signing subdaemon.");
10531068

1069+
opt_register_arg("--experimental-websocket-port",
1070+
opt_set_websocket_port, NULL,
1071+
ld,
1072+
"experimental: alternate port for peers to connect"
1073+
" using WebSockets (RFC6455)");
10541074
opt_register_logging(ld);
10551075
opt_register_version();
10561076

@@ -1463,6 +1483,11 @@ static void add_config(struct lightningd *ld,
14631483
json_add_opt_disable_plugins(response, ld->plugins);
14641484
} else if (opt->cb_arg == (void *)opt_force_feerates) {
14651485
answer = fmt_force_feerates(name0, ld->force_feerates);
1486+
} else if (opt->cb_arg == (void *)opt_set_websocket_port) {
1487+
if (ld->websocket_port)
1488+
json_add_u32(response, name0,
1489+
ld->websocket_port);
1490+
return;
14661491
} else if (opt->cb_arg == (void *)opt_important_plugin) {
14671492
/* Do nothing, this is already handled by
14681493
* opt_add_plugin. */

requirements.lock

+30-30
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# This file is autogenerated by pip-compile with python 3.8
33
# To update, run:
44
#
5-
# pip-compile --output-file=requirements.lock requirements.in
5+
# pip-compile --output-file=requirements.lock requirements.txt
66
#
77
alabaster==0.7.12
88
# via sphinx
@@ -15,9 +15,9 @@ attrs==21.2.0
1515
babel==2.9.1
1616
# via sphinx
1717
base58==2.0.1
18-
# via -r requirements.in
18+
# via pyln.proto
1919
bitstring==3.1.9
20-
# via -r requirements.in
20+
# via pyln.proto
2121
certifi==2021.5.30
2222
# via requests
2323
cffi==1.14.6
@@ -27,33 +27,33 @@ cffi==1.14.6
2727
charset-normalizer==2.0.6
2828
# via requests
2929
cheroot==8.5.2
30-
# via -r requirements.in
30+
# via pyln-testing
3131
click==7.1.2
3232
# via flask
3333
coincurve==13.0.0
34-
# via -r requirements.in
34+
# via pyln.proto
3535
commonmark==0.9.1
3636
# via recommonmark
3737
crc32c==2.2.post0
38-
# via -r requirements.in
38+
# via -r requirements.txt
3939
cryptography==3.4.8
40-
# via -r requirements.in
40+
# via pyln.proto
4141
docutils==0.17.1
4242
# via
4343
# recommonmark
4444
# sphinx
4545
entrypoints==0.3
4646
# via flake8
4747
ephemeral-port-reserve==1.1.1
48-
# via -r requirements.in
48+
# via pyln-testing
4949
execnet==1.9.0
5050
# via pytest-xdist
5151
flake8==3.7.9
52-
# via -r requirements.in
52+
# via -r requirements.txt
5353
flaky==3.7.0
54-
# via -r requirements.in
54+
# via pyln-testing
5555
flask==1.1.4
56-
# via -r requirements.in
56+
# via pyln-testing
5757
idna==3.2
5858
# via requests
5959
imagesize==1.2.0
@@ -70,9 +70,9 @@ jinja2==2.11.3
7070
# mrkd
7171
# sphinx
7272
jsonschema==3.2.0
73-
# via -r requirements.in
73+
# via pyln-testing
7474
mako==1.1.5
75-
# via -r requirements.in
75+
# via -r requirements.txt
7676
markupsafe==2.0.1
7777
# via
7878
# jinja2
@@ -88,9 +88,9 @@ more-itertools==8.10.0
8888
# cheroot
8989
# jaraco.functools
9090
mrkd==0.1.6
91-
# via -r requirements.in
91+
# via -r requirements.txt
9292
mypy==0.910
93-
# via -r requirements.in
93+
# via pyln.proto
9494
mypy-extensions==0.4.3
9595
# via mypy
9696
packaging==21.0
@@ -102,19 +102,17 @@ plac==1.3.3
102102
pluggy==0.13.1
103103
# via pytest
104104
psutil==5.7.3
105-
# via -r requirements.in
105+
# via pyln-testing
106106
psycopg2-binary==2.8.6
107-
# via -r requirements.in
107+
# via pyln-testing
108108
py==1.10.0
109109
# via
110110
# pytest
111111
# pytest-forked
112112
pycodestyle==2.5.0
113113
# via flake8
114114
pycparser==2.20
115-
# via
116-
# -r requirements.in
117-
# cffi
115+
# via cffi
118116
pyflakes==2.1.1
119117
# via flake8
120118
pygments==2.10.0
@@ -126,28 +124,28 @@ pyparsing==2.4.7
126124
pyrsistent==0.18.0
127125
# via jsonschema
128126
pysocks==1.7.1
129-
# via -r requirements.in
127+
# via pyln.proto
130128
pytest==6.1.2
131129
# via
132-
# -r requirements.in
130+
# pyln-testing
133131
# pytest-forked
134132
# pytest-rerunfailures
135133
# pytest-timeout
136134
# pytest-xdist
137135
pytest-forked==1.3.0
138136
# via pytest-xdist
139137
pytest-rerunfailures==9.1.1
140-
# via -r requirements.in
138+
# via pyln-testing
141139
pytest-timeout==1.4.2
142-
# via -r requirements.in
140+
# via pyln-testing
143141
pytest-xdist==2.2.1
144-
# via -r requirements.in
142+
# via pyln-testing
145143
python-bitcoinlib==0.11.0
146-
# via -r requirements.in
144+
# via pyln-testing
147145
pytz==2021.1
148146
# via babel
149147
recommonmark==0.7.1
150-
# via -r requirements.in
148+
# via pyln-client
151149
requests==2.26.0
152150
# via sphinx
153151
six==1.16.0
@@ -171,13 +169,15 @@ sphinxcontrib-qthelp==1.0.3
171169
sphinxcontrib-serializinghtml==1.1.5
172170
# via sphinx
173171
toml==0.10.2
174-
# via
175-
# mypy
176-
# pytest
172+
# via pytest
173+
typed-ast==1.4.3
174+
# via mypy
177175
typing-extensions==3.10.0.2
178176
# via mypy
179177
urllib3==1.26.7
180178
# via requests
179+
websocket-client==1.2.1
180+
# via -r requirements.txt
181181
werkzeug==1.0.1
182182
# via flask
183183

requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
mrkd ~= 0.1.6
33
Mako ~= 1.1.3
44
flake8 ~= 3.7.8
5+
websocket-client
56

67
./contrib/pyln-client
78
./contrib/pyln-proto

tests/test_connection.py

+64
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
from fixtures import * # noqa: F401,F403
33
from fixtures import TEST_NETWORK
44
from flaky import flaky # noqa: F401
5+
from ephemeral_port_reserve import reserve # type: ignore
56
from pyln.client import RpcError, Millisatoshi
7+
import pyln.proto.wire as wire
68
from utils import (
79
only_one, wait_for, sync_blockheight, TIMEOUT,
810
expected_peer_features, expected_node_features,
@@ -20,6 +22,7 @@
2022
import shutil
2123
import time
2224
import unittest
25+
import websocket
2326

2427

2528
def test_connect(node_factory):
@@ -3740,6 +3743,67 @@ def test_old_feerate(node_factory):
37403743
l1.pay(l2, 1000)
37413744

37423745

3746+
@pytest.mark.developer("needs --dev-allow-localhost")
3747+
def test_websocket(node_factory):
3748+
ws_port = reserve()
3749+
l1, l2 = node_factory.line_graph(2,
3750+
opts=[{'experimental-websocket-port': ws_port,
3751+
'dev-allow-localhost': None},
3752+
{'dev-allow-localhost': None}],
3753+
wait_for_announce=True)
3754+
assert l1.rpc.listconfigs()['experimental-websocket-port'] == ws_port
3755+
3756+
# Adapter to turn websocket into a stream "connection"
3757+
class BinWebSocket(object):
3758+
def __init__(self, hostname, port):
3759+
self.ws = websocket.WebSocket()
3760+
self.ws.connect("ws://" + hostname + ":" + str(port))
3761+
self.recvbuf = bytes()
3762+
3763+
def send(self, data):
3764+
self.ws.send(data, websocket.ABNF.OPCODE_BINARY)
3765+
3766+
def recv(self, maxlen):
3767+
while len(self.recvbuf) < maxlen:
3768+
self.recvbuf += self.ws.recv()
3769+
3770+
ret = self.recvbuf[:maxlen]
3771+
self.recvbuf = self.recvbuf[maxlen:]
3772+
return ret
3773+
3774+
ws = BinWebSocket('localhost', ws_port)
3775+
lconn = wire.LightningConnection(ws,
3776+
wire.PublicKey(bytes.fromhex(l1.info['id'])),
3777+
wire.PrivateKey(bytes([1] * 32)),
3778+
is_initiator=True)
3779+
3780+
l1.daemon.wait_for_log('Websocket connection in from')
3781+
3782+
# Perform handshake.
3783+
lconn.shake()
3784+
3785+
# Expect to receive init msg.
3786+
msg = lconn.read_message()
3787+
assert int.from_bytes(msg[0:2], 'big') == 16
3788+
3789+
# Echo same message back.
3790+
lconn.send_message(msg)
3791+
3792+
# Now try sending a ping, ask for 50 bytes
3793+
msg = bytes((0, 18, 0, 50, 0, 0))
3794+
lconn.send_message(msg)
3795+
3796+
# Could actually reply with some gossip msg!
3797+
while True:
3798+
msg = lconn.read_message()
3799+
if int.from_bytes(msg[0:2], 'big') == 19:
3800+
break
3801+
3802+
# Check node_announcement has websocket
3803+
assert (only_one(l2.rpc.listnodes(l1.info['id'])['nodes'])['addresses']
3804+
== [{'type': 'ipv4', 'address': '127.0.0.1', 'port': l1.port}, {'type': 'websocket', 'port': ws_port}])
3805+
3806+
37433807
@pytest.mark.developer("dev-disconnect required")
37443808
def test_ping_timeout(node_factory):
37453809
# Disconnects after this, but doesn't know it.

0 commit comments

Comments
 (0)