diff --git a/AUTHORS b/AUTHORS index f2a0b6b1..82a22ffa 100644 --- a/AUTHORS +++ b/AUTHORS @@ -22,6 +22,7 @@ Paul Banks Petr Skocik Sergey Avseyev Tai Chi Minh Ralph Eastwood * +Tim Hewitt * Yue Xu Zach Banks diff --git a/Makefile.am b/Makefile.am index 3ab8c27f..2fd8dbcc 100644 --- a/Makefile.am +++ b/Makefile.am @@ -67,6 +67,7 @@ libdill_la_SOURCES += \ fd.c \ happyeyeballs.c \ http.c \ + hup.c \ iol.h \ iol.c \ ipc.c \ @@ -144,6 +145,7 @@ check_PROGRAMS += \ tests/http \ tests/ws \ tests/term \ + tests/hup \ tests/happyeyeballs endif diff --git a/http.c b/http.c index 8cb66a46..9bb938bb 100644 --- a/http.c +++ b/http.c @@ -44,7 +44,7 @@ struct dill_http_sock { int u; unsigned int mem : 1; struct dill_suffix_storage suffix_mem; - struct dill_term_storage term_mem; + struct dill_hup_storage hup_mem; char rxbuf[1024]; }; @@ -66,10 +66,10 @@ int dill_http_attach_mem(int s, struct dill_http_storage *mem) { /* Take the ownership of the underlying socket. */ s = dill_hown(s); if(dill_slow(s < 0)) {err = errno; goto error;} - /* Wrap the underlying socket into SUFFIX and TERM protocol. */ + /* Wrap the underlying socket into SUFFIX and HUP protocol. */ s = dill_suffix_attach_mem(s, "\r\n", 2, &obj->suffix_mem); if(dill_slow(s < 0)) {err = errno; goto error;} - s = dill_term_attach_mem(s, NULL, 0, &obj->term_mem); + s = dill_hup_attach_mem(s, NULL, 0, &obj->hup_mem); if(dill_slow(s < 0)) {err = errno; goto error;} /* Create the object. */ obj->hvfs.query = dill_http_hquery; @@ -105,14 +105,14 @@ int dill_http_attach(int s) { int dill_http_done(int s, int64_t deadline) { struct dill_http_sock *obj = dill_hquery(s, dill_http_type); if(dill_slow(!obj)) return -1; - return dill_term_done(obj->u, deadline); + return dill_hup_done(obj->u, deadline); } int dill_http_detach(int s, int64_t deadline) { int err; struct dill_http_sock *obj = dill_hquery(s, dill_http_type); if(dill_slow(!obj)) return -1; - int u = dill_term_detach(obj->u, deadline); + int u = dill_hup_detach(obj->u, deadline); if(dill_slow(u < 0)) {err = errno; goto error;} u = dill_suffix_detach(u, deadline); if(dill_slow(u < 0)) {err = errno; goto error;} diff --git a/hup.c b/hup.c new file mode 100644 index 00000000..ac30ee7d --- /dev/null +++ b/hup.c @@ -0,0 +1,311 @@ +/* + + Copyright (c) 2021 Tim Hewitt + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom + the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. + +*/ + +#include +#include +#include +#include + +#define DILL_DISABLE_RAW_NAMES +#include +#include "iol.h" +#include "utils.h" + +#define DILL_MAX_TERMINATOR_LENGTH 32 + +dill_unique_id(dill_hup_type); + +static void * +dill_hup_hquery(struct dill_hvfs *hvfs, const void *type); + +static void +dill_hup_hclose(struct dill_hvfs *hvfs); + +static int +dill_hup_msendl( + struct dill_msock_vfs *mvfs, + struct dill_iolist *first, + struct dill_iolist *last, + int64_t deadline +); + +static ssize_t +dill_hup_mrecvl( + struct dill_msock_vfs *mvfs, + struct dill_iolist *first, + struct dill_iolist *last, + int64_t deadline +); + +struct dill_hup_sock { + struct dill_hvfs hvfs; + struct dill_msock_vfs mvfs; + int under; + size_t len; + uint8_t buf[DILL_MAX_TERMINATOR_LENGTH]; + unsigned int indone: 1; + unsigned int outdone: 1; + unsigned int sent: 1; + unsigned int ownmem : 1; +}; + +DILL_CHECK_STORAGE(dill_hup_sock, dill_hup_storage) + +#define lest(cond) if (dill_slow(cond)) + +static void * +dill_hup_hquery(struct dill_hvfs *hvfs, const void *type) +{ + struct dill_hup_sock *this = (struct dill_hup_sock *)hvfs; + if (type == dill_msock_type) return &this->mvfs; + if (type == dill_hup_type) return this; + errno = ENOTSUP; + return NULL; +} + +int dill_hup_attach_mem( + int s, + const void *buf, + size_t len, + struct dill_hup_storage *mem +) +{ + int err; + + /* Do we actually have memory and a small enough terminator? */ + lest (!mem || len > DILL_MAX_TERMINATOR_LENGTH) { + err = EINVAL; + goto error; + } + + /* Do we actually have a terminator? */ + lest (len > 0 && !buf) { + err = EINVAL; + goto error; + } + + /* claim the socket */ + s = dill_hown(s); + lest (s < 0) { + err = errno; + goto error; + } + + /* make sure it's an msocket */ + void *q = dill_hquery(s, dill_msock_type); + lest (!q) { + err = errno == ENOTSUP? EPROTO : errno; + goto error; + } + + /* make the darn thing! */ + struct dill_hup_sock *this = (struct dill_hup_sock *)mem; + this->hvfs.query = dill_hup_hquery; + this->hvfs.close = dill_hup_hclose; + this->mvfs.msendl = dill_hup_msendl; + this->mvfs.mrecvl = dill_hup_mrecvl; + this->under = s; + this->len = len; + memcpy(this->buf, buf, len); + this->outdone = 0; + this->indone = 0; + this->sent = 0; + this->ownmem = 0; + + /* wrap it in a handle */ + int h = dill_hmake(&this->hvfs); + lest (h < 0) { + err = errno; + goto error; + } + + return h; + +error: + if (s >= 0) dill_hclose(s); + errno = err; + return -1; +} + +int +dill_hup_attach(int s, const void *buf, size_t len) +{ + int err; + + struct dill_hup_sock *obj = malloc(sizeof(struct dill_hup_sock)); + lest (!obj) { + err = ENOMEM; + goto error1; + } + + s = dill_hup_attach_mem(s, buf, len, (struct dill_hup_storage *)obj); + lest (s < 0) { + err = errno; + goto error2; + } + + obj->ownmem = 1; + return s; + +error2: + free(obj); +error1: + if (s >= 0) dill_hclose(s); + errno = err; + return -1; +} + +int +dill_hup_done(int s, int64_t deadline) +{ + struct dill_hup_sock *this = dill_hquery(s, dill_hup_type); + lest (!this) return -1; + + lest (this->outdone) { + errno = EPIPE; + return -1; + } + + int rc = dill_msend(this->under, this->buf, this->len, deadline); + lest (rc < 0) return -1; + + this->outdone = 1; + return 0; +} + +int +dill_hup_detach(int s, int64_t deadline) +{ + int err; + + struct dill_hup_sock *this = dill_hquery(s, dill_hup_type); + lest(!this) return -1; + + /* Only need to hang up if we sent anything */ + if (this->sent && !this->outdone) { + int rc = dill_hup_done(s, deadline); + lest (rc < 0) { + err = errno; + goto error; + } + } + s = this->under; + if (this->ownmem) free(this); + return s; + +error: /* a declaration is not a statement, but a semicolon is -> */; + int rc = dill_hclose(s); + errno = err; + return -1; +} + +static int +dill_hup_msendl( + struct dill_msock_vfs *mvfs, + struct dill_iolist *first, + struct dill_iolist *last, + int64_t deadline +) +{ + struct dill_hup_sock *this = dill_cont(mvfs, struct dill_hup_sock, mvfs); + + /* A peer cannot send a HUP message after they have claimed to hang up */ + lest (this->outdone) { + errno = EPIPE; + return -1; + } + + /* TODO: compare msg to HUP msg + * (utility function to compare iolists to buffers would be nice...) + */ + int rc = dill_msendl(this->under, first, last, deadline); + if (dill_fast(rc >= 0)) this->sent = 1; + return rc; +} + +static ssize_t +dill_hup_mrecvl( + struct dill_msock_vfs *mvfs, + struct dill_iolist *first, + struct dill_iolist *last, + int64_t deadline +) +{ + struct dill_hup_sock *this = dill_cont(mvfs, struct dill_hup_sock, mvfs); + + /* A peer will not send any more HUP messages after they claim to be done, + * but they may be sending over another protocol, so we must not even + * attempt to read. + */ + lest (this->indone) { + errno = EPIPE; + return -1; + } + + if (this->len == 0) { + ssize_t sz = dill_mrecvl(this->under, first, last, deadline); + lest (sz < 0) return -1; + lest (sz == 0) { + this->indone = 1; + errno = EPIPE; + return -1; + } + return sz; + } + + /* + * Temporarily replace the first this->len bytes in the iolist with a local + * buffer so we can easily compare them with memcmp. + * + * (That buffer-iolist compare function sounds pretty handy...) + */ + struct dill_iolist trimmed = {0}; + int rc = dill_ioltrim(first, this->len, &trimmed); + uint8_t buf[this->len]; + struct dill_iolist iol = {buf, this->len, rc < 0? NULL : &trimmed, 0}; + + ssize_t sz = dill_mrecvl(this->under, &iol, rc < 0? &iol : last, deadline); + lest (sz < 0) return -1; + lest (sz == this->len && memcmp(this->buf, buf, this->len) == 0) { + this->indone = 1; + errno = EPIPE; + return -1; + } + + dill_iolto(buf, this->len, first); + return sz; +} + +static void +dill_hup_hclose(struct dill_hvfs *hvfs) +{ + struct dill_hup_sock *this = (struct dill_hup_sock *)hvfs; + + if (dill_fast(this->under >= 0)) { + int rc = dill_hclose(this->under); + dill_assert(rc == 0); + } + + if (this->ownmem) free(this); +} diff --git a/libdill.h b/libdill.h index 264d273c..a16dbcdb 100644 --- a/libdill.h +++ b/libdill.h @@ -1085,6 +1085,43 @@ DILL_EXPORT int dill_term_detach( #define term_detach dill_term_detach #endif +/******************************************************************************/ +/* HUP protocol. */ +/* Implements half-close behavior on top of any message-based protocol. */ +/* */ +/* WARNING: The HUP protocol is *NOT* safe for ad-hoc use, as it does not */ +/* clean up the read queue when being detached. It is supposed to behave */ +/* this way as a building block for other protocols that clearly define */ +/* which peer should be sending during an exchange. */ +/******************************************************************************/ + +struct dill_hup_storage {char _[88];}; + +DILL_EXPORT int dill_hup_attach( + int s, + const void *buf, + size_t len); +DILL_EXPORT int dill_hup_attach_mem( + int s, + const void *buf, + size_t len, + struct dill_hup_storage *mem); +DILL_EXPORT int dill_hup_done( + int s, + int64_t deadline); +DILL_EXPORT int dill_hup_detach( + int s, + int64_t deadline); + +#if !defined DILL_DISABLE_RAW_NAMES +#define hup_storage dill_hup_storage +#define hup_attach dill_hup_attach +#define hup_attach_mem dill_hup_attach_mem +#define hup_done dill_hup_done +#define hup_detach dill_hup_detach +#endif + + /******************************************************************************/ /* Happy Eyeballs (RFC 8305). */ /* Implements concurrent TCP connecting to the remote endpoint. */ diff --git a/tests/hup.c b/tests/hup.c new file mode 100644 index 00000000..7ea107eb --- /dev/null +++ b/tests/hup.c @@ -0,0 +1,143 @@ +/* + + Copyright (c) 2021 Martin Sustrik, Tim Hewitt + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom + the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. + +*/ + +#include + +#include "assert.h" +#include "../libdill.h" + +coroutine void client(int s) { + s = suffix_attach(s, "\r\n", 2); + errno_assert(s >= 0); + s = hup_attach(s, "STOP", 4); + errno_assert(s >= 0); + int rc = msend(s, "ABC", 3, -1); + errno_assert(rc >= 0); + /* HUP allows a half-closed connection that can still be listened on */ + rc = hup_done(s, -1); + errno_assert(rc == 0); + char buf[16]; + ssize_t sz = mrecv(s, buf, sizeof(buf), -1); + errno_assert(sz >= 0); + assert(sz == 3); + assert(buf[0] == 'D' && buf[1] == 'E' && buf[2] == 'F'); + s = hup_detach(s, -1); + errno_assert(s >= 0); + rc = hclose(s); + errno_assert(rc >= 0); +} + +coroutine void silent_client(int s) +{ + s = suffix_attach(s, "\r\n", 2); + errno_assert(s >= 0); + + s = hup_attach(s, "STOP", 4); + errno_assert(s >= 0); + + char buf[16]; + ssize_t sz = mrecv(s, buf, sizeof(buf), -1); + errno_assert(sz == 3); + assert(buf[0] == 'G' && buf[1] == 'H' && buf[2] == 'I'); + + s = hup_detach(s, -1); + errno_assert(s >= 0); + + /* + * expect the termination message from a noisy peer + * + * HUP peers _should_ read until EPIPE, to know that the peer has said + * their piece. However, because HUP allows one side of a connection to + * detach before the other side has stopped sending, situations like this + * can arise. HUP is only useful as a building block in a larger protocol + * that has enough context to coordinate protocol setup and teardown. + */ + sz = mrecv(s, buf, sizeof(buf), -1); + errno_assert(sz == 4); + assert(buf[0] == 'S' && buf[1] == 'T' && buf[2] == 'O' && buf[3] == 'P'); + + /* wait to allow the server to timeout before just closing the socket */ + int rc = msleep(now() + 200); + + rc = hclose(s); + errno_assert(rc >= 0); +} + +int main(void) { + int p[2]; + int rc = ipc_pair(p); + errno_assert(rc == 0); + int cr = go(client(p[0])); + errno_assert(cr >= 0); + int s = suffix_attach(p[1], "\r\n", 2); + errno_assert(s >= 0); + s = hup_attach(s, "STOP", 4); + errno_assert(s >= 0); + char buf[16]; + ssize_t sz = mrecv(s, buf, sizeof(buf), -1); + errno_assert(sz >= 0); + assert(sz == 3); + assert(buf[0] == 'A' && buf[1] == 'B' && buf[2] == 'C'); + rc = msend(s, "DEF", 3, -1); + errno_assert(rc == 0); + sz = mrecv(s, buf, sizeof(buf), -1); + errno_assert(sz == -1 && errno == EPIPE); + s = hup_detach(s, -1); + errno_assert(s >= 0); + rc = hclose(s); + errno_assert(s >= 0); + rc = bundle_wait(cr, -1); + errno_assert(rc == 0); + rc = hclose(cr); + errno_assert(rc == 0); + + rc = ipc_pair(p); + errno_assert(rc == 0); + + cr = go(silent_client(p[0])); + errno_assert(cr >= 0); + + s = suffix_attach(p[1], "\r\n", 2); + errno_assert(s >= 0); + + s = hup_attach(s, "STOP", 4); + errno_assert(s >= 0); + + rc = msend(s, "GHI", 3, -1); + errno_assert(rc == 0); + + rc = hup_done(s, -1); + errno_assert(rc == 0); + + s = hup_detach(s, -1); + errno_assert(s >= 0); + + /* don't expect termination message from quiet peer */ + sz = mrecv(s, buf, sizeof(buf), 100); + assert(sz < 0); + assert(errno == ETIMEDOUT); + + return 0; +} +