diff --git a/doc/lightning-listconfigs.7.md b/doc/lightning-listconfigs.7.md index 7c71339d50d4..352bd1c409ae 100644 --- a/doc/lightning-listconfigs.7.md +++ b/doc/lightning-listconfigs.7.md @@ -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 @@ -205,4 +206,4 @@ RESOURCES --------- Main web site: -[comment]: # ( SHA256STAMP:7bb40fc8fac201b32d9701b02596d0fa59eb14a3baf606439cbf96dc11548ed4) +[comment]: # ( SHA256STAMP:47c067588120e0f9a71206313685cebb2a8c515e9b04b688b202d2772c8f8146) diff --git a/doc/lightningd-config.5.md b/doc/lightningd-config.5.md index 488db84c9294..05c096944f01 100644 --- a/doc/lightningd-config.5.md +++ b/doc/lightningd-config.5.md @@ -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 ---- diff --git a/doc/schemas/listconfigs.schema.json b/doc/schemas/listconfigs.schema.json index 877cea2107a4..9f910292f9f1 100644 --- a/doc/schemas/listconfigs.schema.json +++ b/doc/schemas/listconfigs.schema.json @@ -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", diff --git a/lightningd/connect_control.c b/lightningd/connect_control.c index 314efccb4599..d17d3e780194 100644 --- a/lightningd/connect_control.c +++ b/lightningd/connect_control.c @@ -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"); @@ -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); diff --git a/lightningd/lightningd.c b/lightningd/lightningd.c index 84f9c9cd5f4d..878856c94c1a 100644 --- a/lightningd/lightningd.c +++ b/lightningd/lightningd.c @@ -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. */ diff --git a/lightningd/lightningd.h b/lightningd/lightningd.h index 274995422500..532841dcab7e 100644 --- a/lightningd/lightningd.h +++ b/lightningd/lightningd.h @@ -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. diff --git a/lightningd/options.c b/lightningd/options.c index 5a163e59eec2..6ea254c423a2 100644 --- a/lightningd/options.c +++ b/lightningd/options.c @@ -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; + 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 */ @@ -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(); @@ -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. */ diff --git a/requirements.txt b/requirements.txt index ef5e2b9dcb5d..4986279a274f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ mrkd ~= 0.1.6 Mako ~= 1.1.3 flake8 ~= 3.7.8 +websocket-client ./contrib/pyln-client ./contrib/pyln-proto diff --git a/tests/test_connection.py b/tests/test_connection.py index 0d9d58ff53f9..a0387c2ee586 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -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, @@ -20,6 +22,7 @@ import shutil import time import unittest +import websocket def test_connect(node_factory): @@ -3740,6 +3743,58 @@ def test_old_feerate(node_factory): l1.pay(l2, 1000) +def test_websocket(node_factory): + ws_port = reserve() + l1 = node_factory.get_node(options={'experimental-websocket-port': ws_port, 'log-level': 'io'}) + 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 + + @pytest.mark.developer("dev-disconnect required") def test_ping_timeout(node_factory): # Disconnects after this, but doesn't know it.