experimental-websocket-port: option to create a WebSocket port.

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
This commit is contained in:
Rusty Russell 2021-10-18 10:43:33 +10:30 committed by Christian Decker
parent 80a47f1111
commit ed6eaf9171
10 changed files with 142 additions and 33 deletions

View File

@ -56,6 +56,7 @@ On success, an object is returned, containing:
- **experimental-onion-messages** (boolean, optional): `experimental-onion-messages` field from config or cmdline, or default - **experimental-onion-messages** (boolean, optional): `experimental-onion-messages` field from config or cmdline, or default
- **experimental-offers** (boolean, optional): `experimental-offers` field from config or cmdline, or default - **experimental-offers** (boolean, optional): `experimental-offers` field from config or cmdline, or default
- **experimental-shutdown-wrong-funding** (boolean, optional): `experimental-shutdown-wrong-funding` field from config or cmdline, or default - **experimental-shutdown-wrong-funding** (boolean, optional): `experimental-shutdown-wrong-funding` field from config or cmdline, or default
- **experimental-websocket-port** (u16, optional): `experimental-websocket-port` field from config or cmdline, or default
- **rgb** (hex, optional): `rgb` field from config or cmdline, or default (always 6 characters) - **rgb** (hex, optional): `rgb` field from config or cmdline, or default (always 6 characters)
- **alias** (string, optional): `alias` field from config or cmdline, or default - **alias** (string, optional): `alias` field from config or cmdline, or default
- **pid-file** (string, optional): `pid-file` field from config or cmdline, or default - **pid-file** (string, optional): `pid-file` field from config or cmdline, or default
@ -205,4 +206,4 @@ RESOURCES
--------- ---------
Main web site: <https://github.com/ElementsProject/lightning> Main web site: <https://github.com/ElementsProject/lightning>
[comment]: # ( SHA256STAMP:7bb40fc8fac201b32d9701b02596d0fa59eb14a3baf606439cbf96dc11548ed4) [comment]: # ( SHA256STAMP:47c067588120e0f9a71206313685cebb2a8c515e9b04b688b202d2772c8f8146)

View File

@ -517,6 +517,13 @@ about whether to add funds or not to a proposed channel is handled
automatically by a plugin that implements the appropriate logic for automatically by a plugin that implements the appropriate logic for
your needs. The default behavior is to not contribute funds. your needs. The default behavior is to not contribute funds.
**experimental-websocket-port**
Specifying this enables support for accepting incoming WebSocket
connections on that port, on any IPv4 and IPv6 addresses you listen
to. The normal protocol is expected to be sent over WebSocket binary
frames once the connection is upgraded.
BUGS BUGS
---- ----

View File

@ -121,6 +121,10 @@
"type": "boolean", "type": "boolean",
"description": "`experimental-shutdown-wrong-funding` field from config or cmdline, or default" "description": "`experimental-shutdown-wrong-funding` field from config or cmdline, or default"
}, },
"experimental-websocket-port": {
"type": "u16",
"description": "`experimental-websocket-port` field from config or cmdline, or default"
},
"rgb": { "rgb": {
"type": "hex", "type": "hex",
"description": "`rgb` field from config or cmdline, or default", "description": "`rgb` field from config or cmdline, or default",

View File

@ -350,7 +350,10 @@ int connectd_init(struct lightningd *ld)
int hsmfd; int hsmfd;
struct wireaddr_internal *wireaddrs = ld->proposed_wireaddr; struct wireaddr_internal *wireaddrs = ld->proposed_wireaddr;
enum addr_listen_announce *listen_announce = ld->proposed_listen_announce; enum addr_listen_announce *listen_announce = ld->proposed_listen_announce;
const char *websocket_helper_path = ""; const char *websocket_helper_path;
websocket_helper_path = subdaemon_path(tmpctx, ld,
"lightning_websocketd");
if (socketpair(AF_LOCAL, SOCK_STREAM, 0, fds) != 0) if (socketpair(AF_LOCAL, SOCK_STREAM, 0, fds) != 0)
fatal("Could not socketpair for connectd<->gossipd"); fatal("Could not socketpair for connectd<->gossipd");
@ -384,7 +387,7 @@ int connectd_init(struct lightningd *ld)
ld->config.use_v3_autotor, ld->config.use_v3_autotor,
ld->config.connection_timeout_secs, ld->config.connection_timeout_secs,
websocket_helper_path, websocket_helper_path,
0); ld->websocket_port);
subd_req(ld->connectd, ld->connectd, take(msg), -1, 0, subd_req(ld->connectd, ld->connectd, take(msg), -1, 0,
connect_init_done, NULL); connect_init_done, NULL);

View File

@ -216,6 +216,7 @@ static struct lightningd *new_lightningd(const tal_t *ctx)
ld->always_use_proxy = false; ld->always_use_proxy = false;
ld->pure_tor_setup = false; ld->pure_tor_setup = false;
ld->tor_service_password = NULL; ld->tor_service_password = NULL;
ld->websocket_port = 0;
/*~ This is initialized later, but the plugin loop examines this, /*~ This is initialized later, but the plugin loop examines this,
* so set it to NULL explicitly now. */ * so set it to NULL explicitly now. */

View File

@ -284,6 +284,9 @@ struct lightningd {
/* Array of (even) TLV types that we should allow. This is required /* Array of (even) TLV types that we should allow. This is required
* since we otherwise would outright reject them. */ * since we otherwise would outright reject them. */
u64 *accept_extra_tlv_types; u64 *accept_extra_tlv_types;
/* EXPERIMENTAL: websocket port if non-zero */
u16 websocket_port;
}; };
/* Turning this on allows a tal allocation to return NULL, rather than aborting. /* Turning this on allows a tal allocation to return NULL, rather than aborting.

View File

@ -850,6 +850,21 @@ static char *opt_set_wumbo(struct lightningd *ld)
return NULL; return NULL;
} }
static char *opt_set_websocket_port(const char *arg, struct lightningd *ld)
{
u32 port COMPILER_WANTS_INIT("9.3.0 -O2");
char *err;
err = opt_set_u32(arg, &port);
if (err)
return err;
ld->websocket_port = port;
if (ld->websocket_port != port)
return tal_fmt(NULL, "'%s' is out of range", arg);
return NULL;
}
static char *opt_set_dual_fund(struct lightningd *ld) static char *opt_set_dual_fund(struct lightningd *ld)
{ {
/* Dual funding implies anchor outputs */ /* Dual funding implies anchor outputs */
@ -1051,6 +1066,11 @@ static void register_opts(struct lightningd *ld)
"--subdaemon=hsmd:remote_signer " "--subdaemon=hsmd:remote_signer "
"would use a hypothetical remote signing subdaemon."); "would use a hypothetical remote signing subdaemon.");
opt_register_arg("--experimental-websocket-port",
opt_set_websocket_port, NULL,
ld,
"experimental: alternate port for peers to connect"
" using WebSockets (RFC6455)");
opt_register_logging(ld); opt_register_logging(ld);
opt_register_version(); opt_register_version();
@ -1463,6 +1483,11 @@ static void add_config(struct lightningd *ld,
json_add_opt_disable_plugins(response, ld->plugins); json_add_opt_disable_plugins(response, ld->plugins);
} else if (opt->cb_arg == (void *)opt_force_feerates) { } else if (opt->cb_arg == (void *)opt_force_feerates) {
answer = fmt_force_feerates(name0, ld->force_feerates); answer = fmt_force_feerates(name0, ld->force_feerates);
} else if (opt->cb_arg == (void *)opt_set_websocket_port) {
if (ld->websocket_port)
json_add_u32(response, name0,
ld->websocket_port);
return;
} else if (opt->cb_arg == (void *)opt_important_plugin) { } else if (opt->cb_arg == (void *)opt_important_plugin) {
/* Do nothing, this is already handled by /* Do nothing, this is already handled by
* opt_add_plugin. */ * opt_add_plugin. */

View File

@ -2,7 +2,7 @@
# This file is autogenerated by pip-compile with python 3.8 # This file is autogenerated by pip-compile with python 3.8
# To update, run: # To update, run:
# #
# pip-compile --output-file=requirements.lock requirements.in # pip-compile --output-file=requirements.lock requirements.txt
# #
alabaster==0.7.12 alabaster==0.7.12
# via sphinx # via sphinx
@ -15,9 +15,9 @@ attrs==21.2.0
babel==2.9.1 babel==2.9.1
# via sphinx # via sphinx
base58==2.0.1 base58==2.0.1
# via -r requirements.in # via pyln.proto
bitstring==3.1.9 bitstring==3.1.9
# via -r requirements.in # via pyln.proto
certifi==2021.5.30 certifi==2021.5.30
# via requests # via requests
cffi==1.14.6 cffi==1.14.6
@ -27,17 +27,17 @@ cffi==1.14.6
charset-normalizer==2.0.6 charset-normalizer==2.0.6
# via requests # via requests
cheroot==8.5.2 cheroot==8.5.2
# via -r requirements.in # via pyln-testing
click==7.1.2 click==7.1.2
# via flask # via flask
coincurve==13.0.0 coincurve==13.0.0
# via -r requirements.in # via pyln.proto
commonmark==0.9.1 commonmark==0.9.1
# via recommonmark # via recommonmark
crc32c==2.2.post0 crc32c==2.2.post0
# via -r requirements.in # via -r requirements.txt
cryptography==3.4.8 cryptography==3.4.8
# via -r requirements.in # via pyln.proto
docutils==0.17.1 docutils==0.17.1
# via # via
# recommonmark # recommonmark
@ -45,15 +45,15 @@ docutils==0.17.1
entrypoints==0.3 entrypoints==0.3
# via flake8 # via flake8
ephemeral-port-reserve==1.1.1 ephemeral-port-reserve==1.1.1
# via -r requirements.in # via pyln-testing
execnet==1.9.0 execnet==1.9.0
# via pytest-xdist # via pytest-xdist
flake8==3.7.9 flake8==3.7.9
# via -r requirements.in # via -r requirements.txt
flaky==3.7.0 flaky==3.7.0
# via -r requirements.in # via pyln-testing
flask==1.1.4 flask==1.1.4
# via -r requirements.in # via pyln-testing
idna==3.2 idna==3.2
# via requests # via requests
imagesize==1.2.0 imagesize==1.2.0
@ -70,9 +70,9 @@ jinja2==2.11.3
# mrkd # mrkd
# sphinx # sphinx
jsonschema==3.2.0 jsonschema==3.2.0
# via -r requirements.in # via pyln-testing
mako==1.1.5 mako==1.1.5
# via -r requirements.in # via -r requirements.txt
markupsafe==2.0.1 markupsafe==2.0.1
# via # via
# jinja2 # jinja2
@ -88,9 +88,9 @@ more-itertools==8.10.0
# cheroot # cheroot
# jaraco.functools # jaraco.functools
mrkd==0.1.6 mrkd==0.1.6
# via -r requirements.in # via -r requirements.txt
mypy==0.910 mypy==0.910
# via -r requirements.in # via pyln.proto
mypy-extensions==0.4.3 mypy-extensions==0.4.3
# via mypy # via mypy
packaging==21.0 packaging==21.0
@ -102,9 +102,9 @@ plac==1.3.3
pluggy==0.13.1 pluggy==0.13.1
# via pytest # via pytest
psutil==5.7.3 psutil==5.7.3
# via -r requirements.in # via pyln-testing
psycopg2-binary==2.8.6 psycopg2-binary==2.8.6
# via -r requirements.in # via pyln-testing
py==1.10.0 py==1.10.0
# via # via
# pytest # pytest
@ -112,9 +112,7 @@ py==1.10.0
pycodestyle==2.5.0 pycodestyle==2.5.0
# via flake8 # via flake8
pycparser==2.20 pycparser==2.20
# via # via cffi
# -r requirements.in
# cffi
pyflakes==2.1.1 pyflakes==2.1.1
# via flake8 # via flake8
pygments==2.10.0 pygments==2.10.0
@ -126,10 +124,10 @@ pyparsing==2.4.7
pyrsistent==0.18.0 pyrsistent==0.18.0
# via jsonschema # via jsonschema
pysocks==1.7.1 pysocks==1.7.1
# via -r requirements.in # via pyln.proto
pytest==6.1.2 pytest==6.1.2
# via # via
# -r requirements.in # pyln-testing
# pytest-forked # pytest-forked
# pytest-rerunfailures # pytest-rerunfailures
# pytest-timeout # pytest-timeout
@ -137,17 +135,17 @@ pytest==6.1.2
pytest-forked==1.3.0 pytest-forked==1.3.0
# via pytest-xdist # via pytest-xdist
pytest-rerunfailures==9.1.1 pytest-rerunfailures==9.1.1
# via -r requirements.in # via pyln-testing
pytest-timeout==1.4.2 pytest-timeout==1.4.2
# via -r requirements.in # via pyln-testing
pytest-xdist==2.2.1 pytest-xdist==2.2.1
# via -r requirements.in # via pyln-testing
python-bitcoinlib==0.11.0 python-bitcoinlib==0.11.0
# via -r requirements.in # via pyln-testing
pytz==2021.1 pytz==2021.1
# via babel # via babel
recommonmark==0.7.1 recommonmark==0.7.1
# via -r requirements.in # via pyln-client
requests==2.26.0 requests==2.26.0
# via sphinx # via sphinx
six==1.16.0 six==1.16.0
@ -171,13 +169,15 @@ sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5 sphinxcontrib-serializinghtml==1.1.5
# via sphinx # via sphinx
toml==0.10.2 toml==0.10.2
# via # via pytest
# mypy typed-ast==1.4.3
# pytest # via mypy
typing-extensions==3.10.0.2 typing-extensions==3.10.0.2
# via mypy # via mypy
urllib3==1.26.7 urllib3==1.26.7
# via requests # via requests
websocket-client==1.2.1
# via -r requirements.txt
werkzeug==1.0.1 werkzeug==1.0.1
# via flask # via flask

View File

@ -2,6 +2,7 @@
mrkd ~= 0.1.6 mrkd ~= 0.1.6
Mako ~= 1.1.3 Mako ~= 1.1.3
flake8 ~= 3.7.8 flake8 ~= 3.7.8
websocket-client
./contrib/pyln-client ./contrib/pyln-client
./contrib/pyln-proto ./contrib/pyln-proto

View File

@ -2,7 +2,9 @@ from collections import namedtuple
from fixtures import * # noqa: F401,F403 from fixtures import * # noqa: F401,F403
from fixtures import TEST_NETWORK from fixtures import TEST_NETWORK
from flaky import flaky # noqa: F401 from flaky import flaky # noqa: F401
from ephemeral_port_reserve import reserve # type: ignore
from pyln.client import RpcError, Millisatoshi from pyln.client import RpcError, Millisatoshi
import pyln.proto.wire as wire
from utils import ( from utils import (
only_one, wait_for, sync_blockheight, TIMEOUT, only_one, wait_for, sync_blockheight, TIMEOUT,
expected_peer_features, expected_node_features, expected_peer_features, expected_node_features,
@ -20,6 +22,7 @@ import re
import shutil import shutil
import time import time
import unittest import unittest
import websocket
def test_connect(node_factory): def test_connect(node_factory):
@ -3740,6 +3743,67 @@ def test_old_feerate(node_factory):
l1.pay(l2, 1000) l1.pay(l2, 1000)
@pytest.mark.developer("needs --dev-allow-localhost")
def test_websocket(node_factory):
ws_port = reserve()
l1, l2 = node_factory.line_graph(2,
opts=[{'experimental-websocket-port': ws_port,
'dev-allow-localhost': None},
{'dev-allow-localhost': None}],
wait_for_announce=True)
assert l1.rpc.listconfigs()['experimental-websocket-port'] == ws_port
# Adapter to turn websocket into a stream "connection"
class BinWebSocket(object):
def __init__(self, hostname, port):
self.ws = websocket.WebSocket()
self.ws.connect("ws://" + hostname + ":" + str(port))
self.recvbuf = bytes()
def send(self, data):
self.ws.send(data, websocket.ABNF.OPCODE_BINARY)
def recv(self, maxlen):
while len(self.recvbuf) < maxlen:
self.recvbuf += self.ws.recv()
ret = self.recvbuf[:maxlen]
self.recvbuf = self.recvbuf[maxlen:]
return ret
ws = BinWebSocket('localhost', ws_port)
lconn = wire.LightningConnection(ws,
wire.PublicKey(bytes.fromhex(l1.info['id'])),
wire.PrivateKey(bytes([1] * 32)),
is_initiator=True)
l1.daemon.wait_for_log('Websocket connection in from')
# Perform handshake.
lconn.shake()
# Expect to receive init msg.
msg = lconn.read_message()
assert int.from_bytes(msg[0:2], 'big') == 16
# Echo same message back.
lconn.send_message(msg)
# Now try sending a ping, ask for 50 bytes
msg = bytes((0, 18, 0, 50, 0, 0))
lconn.send_message(msg)
# Could actually reply with some gossip msg!
while True:
msg = lconn.read_message()
if int.from_bytes(msg[0:2], 'big') == 19:
break
# Check node_announcement has websocket
assert (only_one(l2.rpc.listnodes(l1.info['id'])['nodes'])['addresses']
== [{'type': 'ipv4', 'address': '127.0.0.1', 'port': l1.port}, {'type': 'websocket', 'port': ws_port}])
@pytest.mark.developer("dev-disconnect required") @pytest.mark.developer("dev-disconnect required")
def test_ping_timeout(node_factory): def test_ping_timeout(node_factory):
# Disconnects after this, but doesn't know it. # Disconnects after this, but doesn't know it.