Skip to content

Commit

Permalink
experimental-websocket-port: option to create a WebSocket port.
Browse files Browse the repository at this point in the history
Signed-off-by: Rusty Russell <[email protected]>
  • Loading branch information
rustyrussell authored and cdecker committed Oct 22, 2021
1 parent 80a47f1 commit ed6eaf9
Show file tree
Hide file tree
Showing 10 changed files with 142 additions and 33 deletions.
3 changes: 2 additions & 1 deletion doc/lightning-listconfigs.7.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-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-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)
- **alias** (string, optional): `alias` field from config or cmdline, or default
- **pid-file** (string, optional): `pid-file` field from config or cmdline, or default
Expand Down Expand Up @@ -205,4 +206,4 @@ RESOURCES
---------

Main web site: <https://github.com/ElementsProject/lightning>
[comment]: # ( SHA256STAMP:7bb40fc8fac201b32d9701b02596d0fa59eb14a3baf606439cbf96dc11548ed4)
[comment]: # ( SHA256STAMP:47c067588120e0f9a71206313685cebb2a8c515e9b04b688b202d2772c8f8146)
7 changes: 7 additions & 0 deletions doc/lightningd-config.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
----

Expand Down
4 changes: 4 additions & 0 deletions doc/schemas/listconfigs.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@
"type": "boolean",
"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": {
"type": "hex",
"description": "`rgb` field from config or cmdline, or default",
Expand Down
7 changes: 5 additions & 2 deletions lightningd/connect_control.c
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,10 @@ int connectd_init(struct lightningd *ld)
int hsmfd;
struct wireaddr_internal *wireaddrs = ld->proposed_wireaddr;
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)
fatal("Could not socketpair for connectd<->gossipd");
Expand Down Expand Up @@ -384,7 +387,7 @@ int connectd_init(struct lightningd *ld)
ld->config.use_v3_autotor,
ld->config.connection_timeout_secs,
websocket_helper_path,
0);
ld->websocket_port);

subd_req(ld->connectd, ld->connectd, take(msg), -1, 0,
connect_init_done, NULL);
Expand Down
1 change: 1 addition & 0 deletions lightningd/lightningd.c
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ static struct lightningd *new_lightningd(const tal_t *ctx)
ld->always_use_proxy = false;
ld->pure_tor_setup = false;
ld->tor_service_password = NULL;
ld->websocket_port = 0;

/*~ This is initialized later, but the plugin loop examines this,
* so set it to NULL explicitly now. */
Expand Down
3 changes: 3 additions & 0 deletions lightningd/lightningd.h
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,9 @@ struct lightningd {
/* Array of (even) TLV types that we should allow. This is required
* since we otherwise would outright reject them. */
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.
Expand Down
25 changes: 25 additions & 0 deletions lightningd/options.c
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,21 @@ static char *opt_set_wumbo(struct lightningd *ld)
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)
{
/* Dual funding implies anchor outputs */
Expand Down Expand Up @@ -1051,6 +1066,11 @@ static void register_opts(struct lightningd *ld)
"--subdaemon=hsmd:remote_signer "
"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_version();

Expand Down Expand Up @@ -1463,6 +1483,11 @@ static void add_config(struct lightningd *ld,
json_add_opt_disable_plugins(response, ld->plugins);
} else if (opt->cb_arg == (void *)opt_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) {
/* Do nothing, this is already handled by
* opt_add_plugin. */
Expand Down
60 changes: 30 additions & 30 deletions requirements.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# This file is autogenerated by pip-compile with python 3.8
# To update, run:
#
# pip-compile --output-file=requirements.lock requirements.in
# pip-compile --output-file=requirements.lock requirements.txt
#
alabaster==0.7.12
# via sphinx
Expand All @@ -15,9 +15,9 @@ attrs==21.2.0
babel==2.9.1
# via sphinx
base58==2.0.1
# via -r requirements.in
# via pyln.proto
bitstring==3.1.9
# via -r requirements.in
# via pyln.proto
certifi==2021.5.30
# via requests
cffi==1.14.6
Expand All @@ -27,33 +27,33 @@ cffi==1.14.6
charset-normalizer==2.0.6
# via requests
cheroot==8.5.2
# via -r requirements.in
# via pyln-testing
click==7.1.2
# via flask
coincurve==13.0.0
# via -r requirements.in
# via pyln.proto
commonmark==0.9.1
# via recommonmark
crc32c==2.2.post0
# via -r requirements.in
# via -r requirements.txt
cryptography==3.4.8
# via -r requirements.in
# via pyln.proto
docutils==0.17.1
# via
# recommonmark
# sphinx
entrypoints==0.3
# via flake8
ephemeral-port-reserve==1.1.1
# via -r requirements.in
# via pyln-testing
execnet==1.9.0
# via pytest-xdist
flake8==3.7.9
# via -r requirements.in
# via -r requirements.txt
flaky==3.7.0
# via -r requirements.in
# via pyln-testing
flask==1.1.4
# via -r requirements.in
# via pyln-testing
idna==3.2
# via requests
imagesize==1.2.0
Expand All @@ -70,9 +70,9 @@ jinja2==2.11.3
# mrkd
# sphinx
jsonschema==3.2.0
# via -r requirements.in
# via pyln-testing
mako==1.1.5
# via -r requirements.in
# via -r requirements.txt
markupsafe==2.0.1
# via
# jinja2
Expand All @@ -88,9 +88,9 @@ more-itertools==8.10.0
# cheroot
# jaraco.functools
mrkd==0.1.6
# via -r requirements.in
# via -r requirements.txt
mypy==0.910
# via -r requirements.in
# via pyln.proto
mypy-extensions==0.4.3
# via mypy
packaging==21.0
Expand All @@ -102,19 +102,17 @@ plac==1.3.3
pluggy==0.13.1
# via pytest
psutil==5.7.3
# via -r requirements.in
# via pyln-testing
psycopg2-binary==2.8.6
# via -r requirements.in
# via pyln-testing
py==1.10.0
# via
# pytest
# pytest-forked
pycodestyle==2.5.0
# via flake8
pycparser==2.20
# via
# -r requirements.in
# cffi
# via cffi
pyflakes==2.1.1
# via flake8
pygments==2.10.0
Expand All @@ -126,28 +124,28 @@ pyparsing==2.4.7
pyrsistent==0.18.0
# via jsonschema
pysocks==1.7.1
# via -r requirements.in
# via pyln.proto
pytest==6.1.2
# via
# -r requirements.in
# pyln-testing
# pytest-forked
# pytest-rerunfailures
# pytest-timeout
# pytest-xdist
pytest-forked==1.3.0
# via pytest-xdist
pytest-rerunfailures==9.1.1
# via -r requirements.in
# via pyln-testing
pytest-timeout==1.4.2
# via -r requirements.in
# via pyln-testing
pytest-xdist==2.2.1
# via -r requirements.in
# via pyln-testing
python-bitcoinlib==0.11.0
# via -r requirements.in
# via pyln-testing
pytz==2021.1
# via babel
recommonmark==0.7.1
# via -r requirements.in
# via pyln-client
requests==2.26.0
# via sphinx
six==1.16.0
Expand All @@ -171,13 +169,15 @@ sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
# via sphinx
toml==0.10.2
# via
# mypy
# pytest
# via pytest
typed-ast==1.4.3
# via mypy
typing-extensions==3.10.0.2
# via mypy
urllib3==1.26.7
# via requests
websocket-client==1.2.1
# via -r requirements.txt
werkzeug==1.0.1
# via flask

Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
mrkd ~= 0.1.6
Mako ~= 1.1.3
flake8 ~= 3.7.8
websocket-client

./contrib/pyln-client
./contrib/pyln-proto
Expand Down
64 changes: 64 additions & 0 deletions tests/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
from fixtures import * # noqa: F401,F403
from fixtures import TEST_NETWORK
from flaky import flaky # noqa: F401
from ephemeral_port_reserve import reserve # type: ignore
from pyln.client import RpcError, Millisatoshi
import pyln.proto.wire as wire
from utils import (
only_one, wait_for, sync_blockheight, TIMEOUT,
expected_peer_features, expected_node_features,
Expand All @@ -20,6 +22,7 @@
import shutil
import time
import unittest
import websocket


def test_connect(node_factory):
Expand Down Expand Up @@ -3740,6 +3743,67 @@ def test_old_feerate(node_factory):
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")
def test_ping_timeout(node_factory):
# Disconnects after this, but doesn't know it.
Expand Down

0 comments on commit ed6eaf9

Please sign in to comment.