From 01813e818599a4862ffc20ab2ce3737bf1916d8c Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 26 Aug 2021 12:47:02 +0930 Subject: [PATCH] lightning_websocketd: simple proxy for websockets. WebSocket is a bit weird: 1. It starts like an HTTP connection, but they send special headers. 2. We reply with special headers, one of which involves SHA1 of one of theirs. 3. We are then in WebSocket mode, where each frame starts with a 2-20 byte header. We relay data in a simplistic way: if either side sends something, we read it and relay it synchronously. That avoids any gratuitous buffering. Signed-off-by: Rusty Russell --- Makefile | 3 +- connectd/Makefile | 13 +- connectd/sha1.c | 190 ++++++++++++++++++ connectd/sha1.h | 9 + connectd/test/Makefile | 2 +- connectd/test/run-websocket.c | 181 +++++++++++++++++ connectd/websocketd.c | 352 ++++++++++++++++++++++++++++++++++ 7 files changed, 746 insertions(+), 4 deletions(-) create mode 100644 connectd/sha1.c create mode 100644 connectd/sha1.h create mode 100644 connectd/test/run-websocket.c create mode 100644 connectd/websocketd.c diff --git a/Makefile b/Makefile index 911c33a0c6f5..7649adb82a06 100644 --- a/Makefile +++ b/Makefile @@ -365,7 +365,8 @@ PKGLIBEXEC_PROGRAMS = \ lightningd/lightning_gossipd \ lightningd/lightning_hsmd \ lightningd/lightning_onchaind \ - lightningd/lightning_openingd + lightningd/lightning_openingd \ + lightningd/lightning_websocketd # Don't delete these intermediaries. .PRECIOUS: $(ALL_GEN_HEADERS) $(ALL_GEN_SOURCES) diff --git a/connectd/Makefile b/connectd/Makefile index 397558128e53..60cdf59d1fe2 100644 --- a/connectd/Makefile +++ b/connectd/Makefile @@ -14,10 +14,17 @@ CONNECTD_SRC := $(CONNECTD_HEADERS:.h=.c) connectd/connectd.c CONNECTD_OBJS := $(CONNECTD_SRC:.c=.o) $(CONNECTD_OBJS): $(CONNECTD_HEADERS) +WEBSOCKETD_HEADERS := connectd/sha1.h +WEBSOCKETD_SRC := $(WEBSOCKETD_HEADERS:.h=.c) connectd/websocketd.c + +WEBSOCKETD_OBJS := $(WEBSOCKETD_SRC:.c=.o) +$(WEBSOCKETD_OBJS): $(WEBSOCKETD_HEADERS) + # Make sure these depend on everything. -ALL_C_SOURCES += $(CONNECTD_SRC) -ALL_C_HEADERS += $(CONNECTD_HEADERS) +ALL_C_SOURCES += $(CONNECTD_SRC) $(WEBSOCKETD_SRC) +ALL_C_HEADERS += $(CONNECTD_HEADERS) $(WEBSOCKETD_HEADERS) ALL_PROGRAMS += lightningd/lightning_connectd +ALL_PROGRAMS += lightningd/lightning_websocketd # Here's what lightningd depends on LIGHTNINGD_CONTROL_HEADERS += connectd/connectd_wiregen.h @@ -68,4 +75,6 @@ CONNECTD_COMMON_OBJS := \ lightningd/lightning_connectd: $(CONNECTD_OBJS) $(CONNECTD_COMMON_OBJS) $(BITCOIN_OBJS) $(WIRE_OBJS) $(HSMD_CLIENT_OBJS) +lightningd/lightning_websocketd: $(WEBSOCKETD_OBJS) common/setup.o common/utils.o + include connectd/test/Makefile diff --git a/connectd/sha1.c b/connectd/sha1.c new file mode 100644 index 000000000000..d9e4951978a6 --- /dev/null +++ b/connectd/sha1.c @@ -0,0 +1,190 @@ +/* hex variants removed -- RR */ +#include + +/******************************************************************************* + * Teeny SHA-1 + * + * The below sha1digest() calculates a SHA-1 hash value for a + * specified data buffer and generates a hex representation of the + * result. This implementation is a re-forming of the SHA-1 code at + * https://github.com/jinqiangshou/EncryptionLibrary. + * + * Copyright (c) 2017 CTrabant + * + * License: MIT, see included LICENSE file for details. + * + * To use the sha1digest() function either copy it into an existing + * project source code file or include this file in a project and put + * the declaration (example below) in the sources files where needed. + ******************************************************************************/ + +#include + +/* Declaration: +extern int sha1digest(uint8_t *digest, const uint8_t *data, size_t databytes); +*/ + +/******************************************************************************* + * sha1digest: https://github.com/CTrabant/teeny-sha1 + * + * Calculate the SHA-1 value for supplied data buffer and generate a + * text representation in hexadecimal. + * + * Based on https://github.com/jinqiangshou/EncryptionLibrary, credit + * goes to @jinqiangshou, all new bugs are mine. + * + * @input: + * data -- data to be hashed + * databytes -- bytes in data buffer to be hashed + * + * @output: + * digest -- the result, MUST be at least 20 bytes + * + * @return: 0 on success and non-zero on error. + ******************************************************************************/ +int +sha1digest(uint8_t *digest, const uint8_t *data, size_t databytes) +{ +#define SHA1ROTATELEFT(value, bits) (((value) << (bits)) | ((value) >> (32 - (bits)))) + + uint32_t W[80]; + uint32_t H[] = {0x67452301, + 0xEFCDAB89, + 0x98BADCFE, + 0x10325476, + 0xC3D2E1F0}; + uint32_t a; + uint32_t b; + uint32_t c; + uint32_t d; + uint32_t e; + uint32_t f = 0; + uint32_t k = 0; + + uint32_t idx; + uint32_t lidx; + uint32_t widx; + uint32_t didx = 0; + + int32_t wcount; + uint32_t temp; + uint64_t databits = ((uint64_t)databytes) * 8; + uint32_t loopcount = (databytes + 8) / 64 + 1; + uint32_t tailbytes = 64 * loopcount - databytes; + uint8_t datatail[128] = {0}; + + if (!digest) + return -1; + + if (!data) + return -1; + + /* Pre-processing of data tail (includes padding to fill out 512-bit chunk): + Add bit '1' to end of message (big-endian) + Add 64-bit message length in bits at very end (big-endian) */ + datatail[0] = 0x80; + datatail[tailbytes - 8] = (uint8_t) (databits >> 56 & 0xFF); + datatail[tailbytes - 7] = (uint8_t) (databits >> 48 & 0xFF); + datatail[tailbytes - 6] = (uint8_t) (databits >> 40 & 0xFF); + datatail[tailbytes - 5] = (uint8_t) (databits >> 32 & 0xFF); + datatail[tailbytes - 4] = (uint8_t) (databits >> 24 & 0xFF); + datatail[tailbytes - 3] = (uint8_t) (databits >> 16 & 0xFF); + datatail[tailbytes - 2] = (uint8_t) (databits >> 8 & 0xFF); + datatail[tailbytes - 1] = (uint8_t) (databits >> 0 & 0xFF); + + /* Process each 512-bit chunk */ + for (lidx = 0; lidx < loopcount; lidx++) + { + /* Compute all elements in W */ + memset (W, 0, 80 * sizeof (uint32_t)); + + /* Break 512-bit chunk into sixteen 32-bit, big endian words */ + for (widx = 0; widx <= 15; widx++) + { + wcount = 24; + + /* Copy byte-per byte from specified buffer */ + while (didx < databytes && wcount >= 0) + { + W[widx] += (((uint32_t)data[didx]) << wcount); + didx++; + wcount -= 8; + } + /* Fill out W with padding as needed */ + while (wcount >= 0) + { + W[widx] += (((uint32_t)datatail[didx - databytes]) << wcount); + didx++; + wcount -= 8; + } + } + + /* Extend the sixteen 32-bit words into eighty 32-bit words, with potential optimization from: + "Improving the Performance of the Secure Hash Algorithm (SHA-1)" by Max Locktyukhin */ + for (widx = 16; widx <= 31; widx++) + { + W[widx] = SHA1ROTATELEFT ((W[widx - 3] ^ W[widx - 8] ^ W[widx - 14] ^ W[widx - 16]), 1); + } + for (widx = 32; widx <= 79; widx++) + { + W[widx] = SHA1ROTATELEFT ((W[widx - 6] ^ W[widx - 16] ^ W[widx - 28] ^ W[widx - 32]), 2); + } + + /* Main loop */ + a = H[0]; + b = H[1]; + c = H[2]; + d = H[3]; + e = H[4]; + + for (idx = 0; idx <= 79; idx++) + { + if (idx <= 19) + { + f = (b & c) | ((~b) & d); + k = 0x5A827999; + } + else if (idx >= 20 && idx <= 39) + { + f = b ^ c ^ d; + k = 0x6ED9EBA1; + } + else if (idx >= 40 && idx <= 59) + { + f = (b & c) | (b & d) | (c & d); + k = 0x8F1BBCDC; + } + else if (idx >= 60 && idx <= 79) + { + f = b ^ c ^ d; + k = 0xCA62C1D6; + } + temp = SHA1ROTATELEFT (a, 5) + f + e + k + W[idx]; + e = d; + d = c; + c = SHA1ROTATELEFT (b, 30); + b = a; + a = temp; + } + + H[0] += a; + H[1] += b; + H[2] += c; + H[3] += d; + H[4] += e; + } + + /* Store binary digest in supplied buffer */ + if (digest) + { + for (idx = 0; idx < 5; idx++) + { + digest[idx * 4 + 0] = (uint8_t) (H[idx] >> 24); + digest[idx * 4 + 1] = (uint8_t) (H[idx] >> 16); + digest[idx * 4 + 2] = (uint8_t) (H[idx] >> 8); + digest[idx * 4 + 3] = (uint8_t) (H[idx]); + } + } + + return 0; +} /* End of sha1digest() */ diff --git a/connectd/sha1.h b/connectd/sha1.h new file mode 100644 index 000000000000..45e8ba5bc12c --- /dev/null +++ b/connectd/sha1.h @@ -0,0 +1,9 @@ +#ifndef LIGHTNING_CONNECTD_SHA1_H +#define LIGHTNING_CONNECTD_SHA1_H +#include "config.h" +#include +#include + +extern int sha1digest(uint8_t *digest, const uint8_t *data, size_t databytes); + +#endif /* LIGHTNING_CONNECTD_SHA1_H */ diff --git a/connectd/test/Makefile b/connectd/test/Makefile index fe92b237655c..c25428f51ed1 100644 --- a/connectd/test/Makefile +++ b/connectd/test/Makefile @@ -17,7 +17,7 @@ ALL_TEST_PROGRAMS += $(CONNECTD_TEST_PROGRAMS) $(CONNECTD_TEST_PROGRAMS): $(CONNECTD_TEST_COMMON_OBJS) $(BITCOIN_OBJS) # Test objects depend on ../ src and headers. -$(CONNECTD_TEST_OBJS): $(CONNECTD_HEADERS) $(CONNECTD_SRC) +$(CONNECTD_TEST_OBJS): $(CONNECTD_HEADERS) $(CONNECTD_SRC) $(WEBSOCKETD_HEADERS) $(WEBSOCKETD_SRC) check-units: $(CONNECTD_TEST_PROGRAMS:%=unittest/%) diff --git a/connectd/test/run-websocket.c b/connectd/test/run-websocket.c new file mode 100644 index 000000000000..2f58db53bc1e --- /dev/null +++ b/connectd/test/run-websocket.c @@ -0,0 +1,181 @@ +#include "config.h" +#include +#include +#include +#include +#include + +/* We don't want to actually do io! */ +#define write my_write +#define read my_read +#define write_all my_write_all +#define read_all my_read_all + +static char *my_rbuf, *my_wbuf; +static size_t my_rbuf_off; + +static ssize_t my_read(int fd, void *buf, size_t count) +{ + if (strlen(my_rbuf + my_rbuf_off) < count) + count = strlen(my_rbuf + my_rbuf_off); + memcpy(buf, my_rbuf + my_rbuf_off, count); + my_rbuf_off += count; + return count; +} +static bool my_read_all(int fd, void *buf, size_t count) +{ + my_read(fd, buf, count); + return true; +} + +static ssize_t my_write(int fd, const void *buf, size_t count) +{ + size_t buflen = tal_bytelen(my_wbuf); + tal_resize(&my_wbuf, buflen + count); + memcpy(my_wbuf + buflen, buf, count); + return count; +} +static bool my_write_all(int fd, const void *buf, size_t count) +{ + my_write(fd, buf, count); + return true; +} + +int websocket_main(int argc, char *argv[]); + +#define main websocket_main + #include "../websocketd.c" + #include "../sha1.c" +#undef main + +/* AUTOGENERATED MOCKS START */ +/* Generated stub for amount_asset_is_main */ +bool amount_asset_is_main(struct amount_asset *asset UNNEEDED) +{ fprintf(stderr, "amount_asset_is_main called!\n"); abort(); } +/* Generated stub for amount_asset_to_sat */ +struct amount_sat amount_asset_to_sat(struct amount_asset *asset UNNEEDED) +{ fprintf(stderr, "amount_asset_to_sat called!\n"); abort(); } +/* Generated stub for amount_sat */ +struct amount_sat amount_sat(u64 satoshis UNNEEDED) +{ fprintf(stderr, "amount_sat called!\n"); abort(); } +/* Generated stub for amount_sat_add */ + bool amount_sat_add(struct amount_sat *val UNNEEDED, + struct amount_sat a UNNEEDED, + struct amount_sat b UNNEEDED) +{ fprintf(stderr, "amount_sat_add called!\n"); abort(); } +/* Generated stub for amount_sat_eq */ +bool amount_sat_eq(struct amount_sat a UNNEEDED, struct amount_sat b UNNEEDED) +{ fprintf(stderr, "amount_sat_eq called!\n"); abort(); } +/* Generated stub for amount_sat_greater_eq */ +bool amount_sat_greater_eq(struct amount_sat a UNNEEDED, struct amount_sat b UNNEEDED) +{ fprintf(stderr, "amount_sat_greater_eq called!\n"); abort(); } +/* Generated stub for amount_sat_sub */ + bool amount_sat_sub(struct amount_sat *val UNNEEDED, + struct amount_sat a UNNEEDED, + struct amount_sat b UNNEEDED) +{ fprintf(stderr, "amount_sat_sub called!\n"); abort(); } +/* Generated stub for amount_sat_to_asset */ +struct amount_asset amount_sat_to_asset(struct amount_sat *sat UNNEEDED, const u8 *asset UNNEEDED) +{ fprintf(stderr, "amount_sat_to_asset called!\n"); abort(); } +/* Generated stub for amount_tx_fee */ +struct amount_sat amount_tx_fee(u32 fee_per_kw UNNEEDED, size_t weight UNNEEDED) +{ fprintf(stderr, "amount_tx_fee called!\n"); abort(); } +/* Generated stub for fromwire */ +const u8 *fromwire(const u8 **cursor UNNEEDED, size_t *max UNNEEDED, void *copy UNNEEDED, size_t n UNNEEDED) +{ fprintf(stderr, "fromwire called!\n"); abort(); } +/* Generated stub for fromwire_amount_sat */ +struct amount_sat fromwire_amount_sat(const u8 **cursor UNNEEDED, size_t *max UNNEEDED) +{ fprintf(stderr, "fromwire_amount_sat called!\n"); abort(); } +/* Generated stub for fromwire_bool */ +bool fromwire_bool(const u8 **cursor UNNEEDED, size_t *max UNNEEDED) +{ fprintf(stderr, "fromwire_bool called!\n"); abort(); } +/* Generated stub for fromwire_fail */ +void *fromwire_fail(const u8 **cursor UNNEEDED, size_t *max UNNEEDED) +{ fprintf(stderr, "fromwire_fail called!\n"); abort(); } +/* Generated stub for fromwire_secp256k1_ecdsa_signature */ +void fromwire_secp256k1_ecdsa_signature(const u8 **cursor UNNEEDED, size_t *max UNNEEDED, + secp256k1_ecdsa_signature *signature UNNEEDED) +{ fprintf(stderr, "fromwire_secp256k1_ecdsa_signature called!\n"); abort(); } +/* Generated stub for fromwire_sha256 */ +void fromwire_sha256(const u8 **cursor UNNEEDED, size_t *max UNNEEDED, struct sha256 *sha256 UNNEEDED) +{ fprintf(stderr, "fromwire_sha256 called!\n"); abort(); } +/* Generated stub for fromwire_tal_arrn */ +u8 *fromwire_tal_arrn(const tal_t *ctx UNNEEDED, + const u8 **cursor UNNEEDED, size_t *max UNNEEDED, size_t num UNNEEDED) +{ fprintf(stderr, "fromwire_tal_arrn called!\n"); abort(); } +/* Generated stub for fromwire_u16 */ +u16 fromwire_u16(const u8 **cursor UNNEEDED, size_t *max UNNEEDED) +{ fprintf(stderr, "fromwire_u16 called!\n"); abort(); } +/* Generated stub for fromwire_u32 */ +u32 fromwire_u32(const u8 **cursor UNNEEDED, size_t *max UNNEEDED) +{ fprintf(stderr, "fromwire_u32 called!\n"); abort(); } +/* Generated stub for fromwire_u64 */ +u64 fromwire_u64(const u8 **cursor UNNEEDED, size_t *max UNNEEDED) +{ fprintf(stderr, "fromwire_u64 called!\n"); abort(); } +/* Generated stub for fromwire_u8 */ +u8 fromwire_u8(const u8 **cursor UNNEEDED, size_t *max UNNEEDED) +{ fprintf(stderr, "fromwire_u8 called!\n"); abort(); } +/* Generated stub for fromwire_u8_array */ +void fromwire_u8_array(const u8 **cursor UNNEEDED, size_t *max UNNEEDED, u8 *arr UNNEEDED, size_t num UNNEEDED) +{ fprintf(stderr, "fromwire_u8_array called!\n"); abort(); } +/* Generated stub for towire */ +void towire(u8 **pptr UNNEEDED, const void *data UNNEEDED, size_t len UNNEEDED) +{ fprintf(stderr, "towire called!\n"); abort(); } +/* Generated stub for towire_amount_sat */ +void towire_amount_sat(u8 **pptr UNNEEDED, const struct amount_sat sat UNNEEDED) +{ fprintf(stderr, "towire_amount_sat called!\n"); abort(); } +/* Generated stub for towire_bool */ +void towire_bool(u8 **pptr UNNEEDED, bool v UNNEEDED) +{ fprintf(stderr, "towire_bool called!\n"); abort(); } +/* Generated stub for towire_secp256k1_ecdsa_signature */ +void towire_secp256k1_ecdsa_signature(u8 **pptr UNNEEDED, + const secp256k1_ecdsa_signature *signature UNNEEDED) +{ fprintf(stderr, "towire_secp256k1_ecdsa_signature called!\n"); abort(); } +/* Generated stub for towire_sha256 */ +void towire_sha256(u8 **pptr UNNEEDED, const struct sha256 *sha256 UNNEEDED) +{ fprintf(stderr, "towire_sha256 called!\n"); abort(); } +/* Generated stub for towire_u16 */ +void towire_u16(u8 **pptr UNNEEDED, u16 v UNNEEDED) +{ fprintf(stderr, "towire_u16 called!\n"); abort(); } +/* Generated stub for towire_u32 */ +void towire_u32(u8 **pptr UNNEEDED, u32 v UNNEEDED) +{ fprintf(stderr, "towire_u32 called!\n"); abort(); } +/* Generated stub for towire_u64 */ +void towire_u64(u8 **pptr UNNEEDED, u64 v UNNEEDED) +{ fprintf(stderr, "towire_u64 called!\n"); abort(); } +/* Generated stub for towire_u8 */ +void towire_u8(u8 **pptr UNNEEDED, u8 v UNNEEDED) +{ fprintf(stderr, "towire_u8 called!\n"); abort(); } +/* Generated stub for towire_u8_array */ +void towire_u8_array(u8 **pptr UNNEEDED, const u8 *arr UNNEEDED, size_t num UNNEEDED) +{ fprintf(stderr, "towire_u8_array called!\n"); abort(); } +/* AUTOGENERATED MOCKS END */ + +int main(int argc, char *argv[]) +{ + const char *hdr; + + common_setup(argv[0]); + + hdr = "GET /chat HTTP/1.1\r\n" + "Host: server.example.com\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" + "Origin: http://example.com\r\n" + "Sec-WebSocket-Protocol: chat, superchat\r\n" + "Sec-WebSocket-Version: 13\r\n\r\n"; + + my_rbuf = tal_strdup(tmpctx, hdr); + my_wbuf = tal_arr(tmpctx, char, 0); + + http_upgrade(STDIN_FILENO, ""); + assert(streq(tal_strndup(tmpctx, my_wbuf, tal_bytelen(my_wbuf)), + "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n" + "\r\n")); + + common_shutdown(); +} diff --git a/connectd/websocketd.c b/connectd/websocketd.c new file mode 100644 index 000000000000..97cd4fedae19 --- /dev/null +++ b/connectd/websocketd.c @@ -0,0 +1,352 @@ +/* A simple standalone websocket <-> binary proxy. + * See https://datatracker.ietf.org/doc/html/rfc6455 + */ +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-------+-+-------------+-------------------------------+ + |F|R|R|R| opcode|M| Payload len | Extended payload length | + |I|S|S|S| (4) |A| (7) | (16/64) | + |N|V|V|V| |S| | (if payload len==126/127) | + | |1|2|3| |K| | | + +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + + | Extended payload length continued, if payload len == 127 | + + - - - - - - - - - - - - - - - +-------------------------------+ + | |Masking-key, if MASK set to 1 | + +-------------------------------+-------------------------------+ + | Masking-key (continued) | Payload Data | + +-------------------------------- - - - - - - - - - - - - - - - + + : Payload Data continued ... : + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + | Payload Data continued ... | + +---------------------------------------------------------------+ +*/ + +/* RFC-6455: + + A |Sec-WebSocket-Accept| header field. The value of this header field + is constructed by concatenating /key/, defined above in step 4 in + Section 4.2.2, with the string "258EAFA5- + E914-47DA-95CA-C5AB0DC85B11", taking the SHA-1 hash of this + concatenated value to obtain a 20-byte value and base64- encoding (see + Section 4 of [RFC4648]) this 20-byte hash. + +... + + NOTE: As an example, if the value of the |Sec-WebSocket-Key| header + field in the client's handshake were "dGhlIHNhbXBsZSBub25jZQ==", the + server would append the string "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + to form the string "dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA- + C5AB0DC85B11". The server would then take the SHA-1 hash of this + string, giving the value 0xb3 0x7a 0x4f 0x2c 0xc0 0x62 0x4f 0x16 0x90 + 0xf6 0x46 0x06 0xcf 0x38 0x59 0x45 0xb2 0xbe 0xc4 0xea. This value + is then base64-encoded, to give the value + "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=", which would be returned in the + |Sec-WebSocket-Accept| header field. +*/ +static const char *websocket_accept_str(const tal_t *ctx, const char *key) +{ + u8 sha1[20]; + const char *concat; + char base64[100]; + + concat = tal_fmt(tmpctx, "%s258EAFA5-E914-47DA-95CA-C5AB0DC85B11", + key); + sha1digest(sha1, (const u8 *)concat, strlen(concat)); + if (base64_encode(base64, sizeof(base64), (const char *)sha1, sizeof(sha1)) == -1) + abort(); + + return tal_strdup(ctx, base64); +} + +static void NORETURN PRINTF_FMT(2,3) +bad_http(int fd, const char *fmt, ...) +{ + va_list ap; + char *resp; + + resp = tal_strdup(tmpctx, "HTTP/1.1 400 I only speak websocket\r\n\r\n"); + va_start(ap, fmt); + tal_append_vfmt(&resp, fmt, ap); + va_end(ap); + + write_all(fd, resp, strlen(resp)); + exit(1); +} + +/* We know headers are terminated by \r\n\r\n at this point */ +static const char *get_http_hdr(const tal_t *ctx, const u8 *buf, size_t buflen, + const char *hdrname) +{ + size_t hdrlen; + + for (;;) { + const u8 *end = memmem(buf, buflen, "\r\n", 2); + hdrlen = end - buf; + + /* Empty line? End of headers. */ + if (hdrlen == 0) + return NULL; + /* header name followed by : */ + if (memstarts(buf, hdrlen, hdrname, strlen(hdrname)) + && buf[strlen(hdrname)] == ':') + break; + buf = end + 2; + } + + buf += strlen(hdrname) + 1; + hdrlen -= strlen(hdrname) + 1; + + /* Ignore leading whitespace (technically, they can split + * fields over multiple lines, but that's silly for the fields + * we're dealing with, so Naah). */ + while (hdrlen && cisspace(*buf)) { + buf++; + hdrlen--; + } + + return tal_strndup(ctx, (const char *)buf, hdrlen); +} + +static bool http_headers_complete(const u8 *buf, size_t len) +{ + return memmem(buf, len, "\r\n\r\n", 4) != NULL; +} + +static void http_respond(int fd, const u8 *buf, size_t len) +{ + const char *hdr; + char *resp; + + /* RFC-6455: + + The client's opening handshake consists of the following + parts. If the server, while reading the handshake, finds + that the client did not send a handshake that matches the + description below ... the server MUST stop processing the + client's handshake and return an HTTP response with an + appropriate error code (such as 400 Bad Request). + + 1. An HTTP/1.1 or higher GET request, including a "Request-URI" + [RFC2616] that should be interpreted as a /resource name/ + defined in Section 3 (or an absolute HTTP/HTTPS URI containing + the /resource name/). + + 2. A |Host| header field containing the server's authority. + + 3. An |Upgrade| header field containing the value "websocket", + treated as an ASCII case-insensitive value. + + 4. A |Connection| header field that includes the token "Upgrade", + treated as an ASCII case-insensitive value. + + 5. A |Sec-WebSocket-Key| header field with a base64-encoded (see + Section 4 of [RFC4648]) value that, when decoded, is 16 bytes in + length. + + 6. A |Sec-WebSocket-Version| header field, with a value of 13. + */ + hdr = get_http_hdr(tmpctx, buf, len, "Upgrade"); + if (!hdr || !strstr(hdr, "websocket")) + bad_http(fd, "Upgrade: websocket missing"); + hdr = get_http_hdr(tmpctx, buf, len, "Connection"); + if (!hdr || !strstr(hdr, "Upgrade")) + bad_http(fd, "Connection: Upgrade missing"); + hdr = get_http_hdr(tmpctx, buf, len, "Sec-WebSocket-Version"); + if (!hdr || !streq(hdr, "13")) + bad_http(fd, "Sec-WebSocket-Version: must be 13"); + hdr = get_http_hdr(tmpctx, buf, len, "Sec-WebSocket-Key"); + if (!hdr) + bad_http(fd, "Sec-WebSocket-Key missing"); + + resp = tal_fmt(tmpctx, + "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: %s\r\n\r\n", + websocket_accept_str(tmpctx, hdr)); + + if (!write_all(fd, resp, strlen(resp))) + exit(0); +} + +static void http_upgrade(int fd, const char *hexstart) +{ + u8 buf[65536]; + size_t len = hex_data_size(strlen(hexstart)); + + if (len > sizeof(buf)) + errx(1, "Unacceptable datasize"); + if (!hex_decode(hexstart, strlen(hexstart), buf, len)) + errx(1, "Bad hex"); + + alarm(60); + while (!http_headers_complete(buf, len)) { + int r; + r = read(STDIN_FILENO, buf + len, sizeof(buf) - len); + if (r <= 0) + bad_http(STDIN_FILENO, "No header end after %zu bytes", + len); + len += r; + } + http_respond(STDIN_FILENO, buf, len); + alarm(0); +} + +static void lightningd_to_websocket(int lightningfd, int wsfd) +{ + /* We prepend ws header */ + u8 buf[4 + 65535]; + int len; + /* Not continued frame (0x80), opcode = 2 (binary) */ + const u8 firstbyte = 0x82; + size_t off; + + len = read(lightningfd, 4 + buf, sizeof(buf) - 4); + if (len <= 0) + exit(0); + + if (len > 125) { + buf[0] = firstbyte; + buf[1] = 126; + buf[2] = (len >> 8); + buf[3] = len; + off = 0; + len += 4; + } else { + buf[2] = firstbyte; + buf[3] = len; + off = 2; + len += 2; + } + if (!write_all(wsfd, buf + off, len)) + exit(0); +} + +/* Returns payload size, sets inmask, is_binframe */ +static size_t read_payload_header(int fd, u8 inmask[4], bool *is_binframe) +{ + /* Worst case header. */ + u8 frame_hdr[20]; + bool mask_set; + size_t hdrsize = 2, len; + + /* First two bytes define hdr size. */ + if (!read_all(fd, frame_hdr, 2)) + exit(0); + + /* RFC-6455: + * %x2 denotes a binary frame + */ + *is_binframe = ((frame_hdr[0] & 0x0F) == 2); + mask_set = (frame_hdr[1] & 0x80); + len = (frame_hdr[1] & 0x7f); + + if (len == 126) + hdrsize += 2; + else if (len == 127) + hdrsize += 8; + + if (mask_set) + hdrsize += 4; + + /* Read rest of hdr if necessary */ + if (hdrsize > 2 && !read_all(fd, frame_hdr + 2, hdrsize - 2)) + exit(0); + + if (len == 126) { + be16 be16len; + memcpy(&be16len, frame_hdr + 2, 2); + len = be16_to_cpu(be16len); + } else if (len == 127) { + be64 be64len; + memcpy(&be64len, frame_hdr + 2, 8); + len = be64_to_cpu(be64len); + } + + if (mask_set) { + memcpy(inmask, frame_hdr + hdrsize - 4, 4); + hdrsize += 4; + } else + memset(inmask, 0, 4); + + return len; +} + +static void apply_mask(u8 *buf, size_t len, const u8 inmask[4]) +{ + for (size_t i = 0; i < len; i++) + buf[i] ^= inmask[i % 4]; +} + +static void websocket_to_lightningd(int wsfd, int lightningfd) +{ + size_t len; + u8 inmask[4]; + bool is_binframe; + + len = read_payload_header(wsfd, inmask, &is_binframe); + while (len > 0) { + u8 buf[65536]; + int rlen = len; + + if (rlen > sizeof(buf)) + rlen = sizeof(buf); + + rlen = read(wsfd, buf, rlen); + if (rlen <= 0) + exit(0); + apply_mask(buf, rlen, inmask); + len -= rlen; + /* We ignore non binary frames (FIXME: Send error!) */ + if (is_binframe && !write_all(lightningfd, buf, rlen)) + exit(0); + } +} + +/* stdin goes to the client, stdout goes to lightningd */ +int main(int argc, char *argv[]) +{ + struct pollfd pfds[2]; + + common_setup(argv[0]); + + if (argc != 2) + errx(1, "Usage: %s ", argv[0]); + + /* Do HTTP-style negotiation to get into websocket frames. */ + http_upgrade(STDIN_FILENO, argv[1]); + + pfds[0].fd = STDIN_FILENO; + pfds[0].events = POLLIN; + pfds[1].fd = STDOUT_FILENO; + pfds[1].events = POLLIN; + + for (;;) { + poll(pfds, 2, -1); + + if (pfds[1].revents & POLLIN) + lightningd_to_websocket(STDOUT_FILENO, STDIN_FILENO); + if (pfds[0].revents & POLLIN) + websocket_to_lightningd(STDIN_FILENO, STDOUT_FILENO); + } + + common_shutdown(); + exit(0); +}