From c8967088ea3268c1fa0449b38a7ab77e9571d5ee Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Mon, 23 Aug 2021 19:53:03 +0930 Subject: [PATCH] experimental-websocket: option to enable autodetection of WebSocket transport. Signed-off-by: Rusty Russell --- common/features.c | 4 ++- common/features.h | 6 ++++ doc/lightning-listconfigs.7 | 6 ++-- doc/lightning-listconfigs.7.md | 5 +-- doc/lightningd-config.5 | 12 +++++-- doc/lightningd-config.5.md | 6 ++++ doc/schemas/listconfigs.schema.json | 4 +++ lightningd/connect_control.c | 9 +++++- lightningd/options.c | 17 ++++++++++ requirements.txt | 1 + tests/test_connection.py | 50 +++++++++++++++++++++++++++++ 11 files changed, 111 insertions(+), 9 deletions(-) diff --git a/common/features.c b/common/features.c index 5e41d056ae10..7fb54f91ff55 100644 --- a/common/features.c +++ b/common/features.c @@ -90,6 +90,8 @@ static const struct feature_style feature_styles[] = { .copy_style = { [INIT_FEATURE] = FEATURE_REPRESENT, [NODE_ANNOUNCE_FEATURE] = FEATURE_REPRESENT, [CHANNEL_FEATURE] = FEATURE_DONT_REPRESENT } }, + { OPT_WEBSOCKET, + .copy_style = { [NODE_ANNOUNCE_FEATURE] = FEATURE_REPRESENT } }, }; struct dependency { @@ -399,7 +401,7 @@ static const char *feature_name(const tal_t *ctx, size_t f) NULL, "option_onion_messages", /* 38/39 */ NULL, /* 40/41 */ - NULL, + "option_websocket", NULL, NULL, NULL, diff --git a/common/features.h b/common/features.h index 176dea86e524..354e5498da44 100644 --- a/common/features.h +++ b/common/features.h @@ -135,4 +135,10 @@ const char *fmt_featurebits(const tal_t *ctx, const u8 *featurebits); #define OPT_SHUTDOWN_WRONG_FUNDING 104 +/* BOLT-websocket #9: + * + * | 42/43 | `option_websocket` |... N ... + */ +#define OPT_WEBSOCKET 42 + #endif /* LIGHTNING_COMMON_FEATURES_H */ diff --git a/doc/lightning-listconfigs.7 b/doc/lightning-listconfigs.7 index 6ae3b742ac9e..64ee57a6c3f5 100644 --- a/doc/lightning-listconfigs.7 +++ b/doc/lightning-listconfigs.7 @@ -92,6 +92,8 @@ On success, an object is returned, containing: .IP \[bu] \fBexperimental-shutdown-wrong-funding\fR (boolean, optional): \fBexperimental-shutdown-wrong-funding\fR field from config or cmdline, or default .IP \[bu] +\fBexperimental-websocket\fR (boolean, optional): \fBexperimental-websocket\fR field from config or cmdline, or default +.IP \[bu] \fBrgb\fR (hex, optional): \fBrgb\fR field from config or cmdline, or default (always 6 characters) .IP \[bu] \fBalias\fR (string, optional): \fBalias\fR field from config or cmdline, or default @@ -148,7 +150,7 @@ On success, an object is returned, containing: .IP \[bu] \fBlog-timestamps\fR (boolean, optional): \fBlog-timestamps\fR field from config or cmdline, or default .IP \[bu] -\fBforce-feerates\fR (string, optional): \fBforce-feerates\fR field from config or cmdline, if any +\fBforce-feerates\fR (string, optional): force-feerate configuration setting, if any .IP \[bu] \fBsubdaemon\fR (string, optional): \fBsubdaemon\fR fields from config or cmdline if any (can be more than one) .IP \[bu] @@ -270,4 +272,4 @@ Vincenzo Palazzo \fI wrote the initial versi Main web site: \fIhttps://github.com/ElementsProject/lightning\fR -\" SHA256STAMP:4591f6c754162b2dcdf82a36d584a48795752d39a986bc1d39c49e0cdbea440f +\" SHA256STAMP:65a9f67550dbd4a25a8efd5264e1a6d387e02448af5d62e3202cd610a0e523a0 diff --git a/doc/lightning-listconfigs.7.md b/doc/lightning-listconfigs.7.md index 85301e074552..199ccdbd654e 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** (boolean, optional): `experimental-websocket` 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 @@ -84,7 +85,7 @@ On success, an object is returned, containing: - **log-prefix** (string, optional): `log-prefix` field from config or cmdline, or default - **log-file** (string, optional): `log-file` field from config or cmdline, or default - **log-timestamps** (boolean, optional): `log-timestamps` field from config or cmdline, or default -- **force-feerates** (string, optional): `force-feerates` field from config or cmdline, if any +- **force-feerates** (string, optional): force-feerate configuration setting, if any - **subdaemon** (string, optional): `subdaemon` fields from config or cmdline if any (can be more than one) - **tor-service-password** (string, optional): `tor-service-password` field from config or cmdline, if any [comment]: # (GENERATE-FROM-SCHEMA-END) @@ -204,4 +205,4 @@ RESOURCES --------- Main web site: -[comment]: # ( SHA256STAMP:ad98179a7b6254a936d4fde179918b6a975e186adcbc396917a0c2ed2888519e) +[comment]: # ( SHA256STAMP:6083722fadd8b5724326fc2685f05b5d056c0a6ab0752af64b10294173d1f9a2) diff --git a/doc/lightningd-config.5 b/doc/lightningd-config.5 index 0049fe154ecc..8d82a8177d6f 100644 --- a/doc/lightningd-config.5 +++ b/doc/lightningd-config.5 @@ -145,7 +145,6 @@ What log level to print out: options are io, debug, info, unusual, broken\. If \fISUBSYSTEM\fR is supplied, this sets the logging level for any subsystem containing that string\. Subsystems include: - .RS .IP \[bu] \fIlightningd\fR: The main lightning daemon @@ -171,7 +170,6 @@ for any subsystem containing that string\. Subsystems include: The following subsystems exist for each channel, where N is an incrementing internal integer id assigned for the lifetime of the channel: - .RS .IP \[bu] \fIopeningd-chan#N\fR: Each opening / idling daemon @@ -627,6 +625,14 @@ 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\. + + \fBexperimental-websocket\fR + + +Specifying this enables support for accepting incoming WebSocket +connections: the normal protocol is expected to be sent over +WebSocket binary frames once the connection is upgraded\. + .SH BUGS You should report bugs on our github issues page, and maybe submit a fix @@ -652,4 +658,4 @@ Main web site: \fIhttps://github.com/ElementsProject/lightning\fR Note: the modules in the ccan/ directory have their own licenses, but the rest of the code is covered by the BSD-style MIT license\. -\" SHA256STAMP:1c392f3fee66dc6c1fc2c34200204a9be1d79e53fd5fb1720ad169fc671f71c0 +\" SHA256STAMP:114b4e458af6112500f2f01210760f1577f9e82fc74e03b6d821f669287c93b2 diff --git a/doc/lightningd-config.5.md b/doc/lightningd-config.5.md index 488db84c9294..410e8ecdcd50 100644 --- a/doc/lightningd-config.5.md +++ b/doc/lightningd-config.5.md @@ -517,6 +517,12 @@ 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** + +Specifying this enables support for accepting incoming WebSocket +connections: 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..0e748ee028ae 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": { + "type": "boolean", + "description": "`experimental-websocket` 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 b15e258a9645..bafe9755557b 100644 --- a/lightningd/connect_control.c +++ b/lightningd/connect_control.c @@ -363,7 +363,14 @@ 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 = NULL; + const char *websocket_helper_path; + + if (feature_offered(ld->our_features->bits[NODE_ANNOUNCE_FEATURE], + OPT_WEBSOCKET)) + websocket_helper_path = subdaemon_path(tmpctx, ld, + "lightning_websocketd"); + else + websocket_helper_path = NULL; if (socketpair(AF_LOCAL, SOCK_STREAM, 0, fds) != 0) fatal("Could not socketpair for connectd<->gossipd"); diff --git a/lightningd/options.c b/lightningd/options.c index aff8b090b414..00221e9e76ca 100644 --- a/lightningd/options.c +++ b/lightningd/options.c @@ -861,6 +861,14 @@ static char *opt_set_wumbo(struct lightningd *ld) return NULL; } +static char *opt_set_websocket(struct lightningd *ld) +{ + feature_set_or(ld->our_features, + take(feature_set_for_feature(NULL, + OPTIONAL_FEATURE(OPT_WEBSOCKET)))); + return NULL; +} + static char *opt_set_dual_fund(struct lightningd *ld) { /* Dual funding implies anchor outputs */ @@ -942,6 +950,10 @@ static void register_opts(struct lightningd *ld) " and allow peers to establish channels" " via v2 channel open protocol."); + opt_register_early_noarg("--experimental-websocket", + opt_set_websocket, ld, + "experimental: allow peers to connect using" + " WebSockets (RFC6455)"); /* This affects our features, so set early. */ opt_register_early_noarg("--experimental-onion-messages", opt_set_onion_messages, ld, @@ -1386,6 +1398,11 @@ static void add_config(struct lightningd *ld, feature_offered(ld->our_features ->bits[INIT_FEATURE], OPT_DUAL_FUND)); + } else if (opt->cb == (void *)opt_set_websocket) { + json_add_bool(response, name0, + feature_offered(ld->our_features + ->bits[NODE_ANNOUNCE_FEATURE], + OPT_WEBSOCKET)); } else if (opt->cb == (void *)opt_set_onion_messages) { json_add_bool(response, name0, feature_offered(ld->our_features 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 a3ba2fd379d5..ba1c35599a80 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -3,6 +3,7 @@ from fixtures import TEST_NETWORK from flaky import flaky # noqa: F401 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 +21,7 @@ import shutil import time import unittest +import websocket def test_connect(node_factory): @@ -3734,3 +3736,51 @@ def test_old_feerate(node_factory): # This will timeout if l2 didn't accept fee. l1.pay(l2, 1000) + + +def test_websocket(node_factory): + l1 = node_factory.get_node(options={'experimental-websocket': None, 'log-level': 'io'}) + assert l1.rpc.listconfigs()['experimental-websocket'] + + # 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', l1.port) + lconn = wire.LightningConnection(ws, + wire.PublicKey(bytes.fromhex(l1.info['id'])), + wire.PrivateKey(bytes([1] * 32)), + is_initiator=True) + # 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