OverviewFilesCommitsIssuesDocsReleasesPeers

bare-abstract-socket / binding.c

371 lines · c ref:
// bare-abstract-socket — Linux abstract-namespace AF_UNIX sockets for bare.
//
// This module owns ONLY the socket setup: it creates abstract-namespace sockets
// (leading-NUL sun_path, which SELinux permits no-root where pathname sockets do
// not) and hands raw fds back to JS. All stream I/O is done by upstream, stock
// `bare-pipe` via `new Pipe(fd)` — we do not touch or fork it.
//
//   connect(name)                    -> a connected client fd (int)
//   listen(name, backlog, ctx, onc)  -> a server handle; onc(fd) per accepted conn
//   close(handle)                    -> stop polling + close the listening fd
//
// Server accept is integrated with the event loop via uv_poll on the listening
// fd (readable -> accept() the backlog). Abstract addressing is plain POSIX
// sockaddr_un, so none of libuv's name-length handling is involved.

#include <assert.h>
#include <bare.h>
#include <errno.h>
#include <fcntl.h>
#include <js.h>
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <uv.h>

typedef struct {
  uv_poll_t poll;
  int fd;

  js_env_t *env;
  js_ref_t *ctx;
  js_ref_t *on_connection;

  bool closing;

  js_deferred_teardown_t *teardown;
} bare_abstract_server_t;

// Fill an abstract-namespace sockaddr_un. Returns the address length to pass to
// bind()/connect(), or -1 if the name is too long. The abstract name is a
// leading NUL byte followed by `name_len` bytes (NOT NUL-terminated); the
// address length is what distinguishes the bytes, so we must pass it exactly.
static int
bare_abstract__fill_addr(struct sockaddr_un *addr, const char *name, size_t name_len) {
  if (name_len + 1 > sizeof(addr->sun_path)) return -1;

  memset(addr, 0, sizeof(*addr));
  addr->sun_family = AF_UNIX;
  addr->sun_path[0] = '\0';
  memcpy(addr->sun_path + 1, name, name_len);

  return (int) (offsetof(struct sockaddr_un, sun_path) + 1 + name_len);
}

static int
bare_abstract__set_nonblock(int fd) {
  int flags = fcntl(fd, F_GETFL, 0);
  if (flags < 0) return -1;
  return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

// uv_poll callback: the listening fd is readable -> drain the accept backlog,
// emitting each connected fd to JS. bare-pipe (via new Pipe(fd)) then does I/O.
static void
bare_abstract__on_poll(uv_poll_t *handle, int status, int events) {
  int err;

  bare_abstract_server_t *server = (bare_abstract_server_t *) handle->data;

  if (server->closing) return;

  js_env_t *env = server->env;

  js_handle_scope_t *scope;
  err = js_open_handle_scope(env, &scope);
  assert(err == 0);

  js_value_t *ctx;
  err = js_get_reference_value(env, server->ctx, &ctx);
  assert(err == 0);

  js_value_t *on_connection;
  err = js_get_reference_value(env, server->on_connection, &on_connection);
  assert(err == 0);

  if (status < 0) {
    js_value_t *argv[2];

    err = js_create_string_utf8(env, (utf8_t *) uv_err_name(status), -1, &argv[0]);
    assert(err == 0);

    err = js_get_null(env, &argv[1]);
    assert(err == 0);

    js_call_function(env, ctx, on_connection, 2, argv, NULL);

    err = js_close_handle_scope(env, scope);
    assert(err == 0);

    return;
  }

  // Drain everything currently pending so we never miss a connection between
  // poll wakeups.
  while (!server->closing) {
    int fd = accept(server->fd, NULL, NULL);

    if (fd < 0) {
      if (errno == EAGAIN || errno == EWOULDBLOCK) break;
      if (errno == EINTR || errno == ECONNABORTED) continue;
      break; // other errors: stop draining this wakeup
    }

    bare_abstract__set_nonblock(fd);

    js_value_t *argv[2];

    err = js_get_null(env, &argv[0]);
    assert(err == 0);

    err = js_create_uint32(env, (uint32_t) fd, &argv[1]);
    assert(err == 0);

    js_call_function(env, ctx, on_connection, 2, argv, NULL);
  }

  err = js_close_handle_scope(env, scope);
  assert(err == 0);
}

static void
bare_abstract__on_close(uv_handle_t *handle) {
  int err;

  bare_abstract_server_t *server = (bare_abstract_server_t *) handle->data;

  js_env_t *env = server->env;

  js_deferred_teardown_t *teardown = server->teardown;

  err = js_delete_reference(env, server->on_connection);
  assert(err == 0);

  err = js_delete_reference(env, server->ctx);
  assert(err == 0);

  err = js_finish_deferred_teardown_callback(teardown);
  assert(err == 0);
}

static void
bare_abstract__teardown(js_deferred_teardown_t *handle, void *data) {
  bare_abstract_server_t *server = (bare_abstract_server_t *) data;

  if (server->closing) return;

  server->closing = true;

  if (server->fd >= 0) {
    close(server->fd);
    server->fd = -1;
  }

  uv_close((uv_handle_t *) &server->poll, bare_abstract__on_close);
}

static js_value_t *
bare_abstract_connect(js_env_t *env, js_callback_info_t *info) {
  int err;

  size_t argc = 1;
  js_value_t *argv[1];

  err = js_get_callback_info(env, info, &argc, argv, NULL, NULL);
  assert(err == 0);

  assert(argc == 1);

  char name[sizeof(((struct sockaddr_un *) 0)->sun_path)];
  size_t name_len;
  err = js_get_value_string_utf8(env, argv[0], (utf8_t *) name, sizeof(name), &name_len);
  assert(err == 0);

  struct sockaddr_un addr;
  int addrlen = bare_abstract__fill_addr(&addr, name, name_len);

  if (addrlen < 0) {
    err = js_throw_error(env, "ENAMETOOLONG", "abstract socket name too long");
    assert(err == 0);
    return NULL;
  }

  int fd = socket(AF_UNIX, SOCK_STREAM, 0);

  if (fd < 0) {
    err = js_throw_error(env, uv_err_name(uv_translate_sys_error(errno)), strerror(errno));
    assert(err == 0);
    return NULL;
  }

  // AF_UNIX connect completes (or is refused) immediately for a listening peer,
  // so a blocking connect never stalls the loop; switch to non-blocking after,
  // as uv_pipe_open (bare-pipe) expects.
  if (connect(fd, (struct sockaddr *) &addr, addrlen) < 0) {
    int e = errno;
    close(fd);
    err = js_throw_error(env, uv_err_name(uv_translate_sys_error(e)), strerror(e));
    assert(err == 0);
    return NULL;
  }

  bare_abstract__set_nonblock(fd);

  js_value_t *result;
  err = js_create_uint32(env, (uint32_t) fd, &result);
  assert(err == 0);

  return result;
}

static js_value_t *
bare_abstract_listen(js_env_t *env, js_callback_info_t *info) {
  int err;

  size_t argc = 4;
  js_value_t *argv[4];

  err = js_get_callback_info(env, info, &argc, argv, NULL, NULL);
  assert(err == 0);

  assert(argc == 4);

  char name[sizeof(((struct sockaddr_un *) 0)->sun_path)];
  size_t name_len;
  err = js_get_value_string_utf8(env, argv[0], (utf8_t *) name, sizeof(name), &name_len);
  assert(err == 0);

  uint32_t backlog;
  err = js_get_value_uint32(env, argv[1], &backlog);
  assert(err == 0);

  struct sockaddr_un addr;
  int addrlen = bare_abstract__fill_addr(&addr, name, name_len);

  if (addrlen < 0) {
    err = js_throw_error(env, "ENAMETOOLONG", "abstract socket name too long");
    assert(err == 0);
    return NULL;
  }

  int fd = socket(AF_UNIX, SOCK_STREAM, 0);

  if (fd < 0) {
    err = js_throw_error(env, uv_err_name(uv_translate_sys_error(errno)), strerror(errno));
    assert(err == 0);
    return NULL;
  }

  if (bind(fd, (struct sockaddr *) &addr, addrlen) < 0) {
    int e = errno;
    close(fd);
    err = js_throw_error(env, uv_err_name(uv_translate_sys_error(e)), strerror(e));
    assert(err == 0);
    return NULL;
  }

  if (listen(fd, (int) backlog) < 0) {
    int e = errno;
    close(fd);
    err = js_throw_error(env, uv_err_name(uv_translate_sys_error(e)), strerror(e));
    assert(err == 0);
    return NULL;
  }

  bare_abstract__set_nonblock(fd);

  js_value_t *handle;
  bare_abstract_server_t *server;
  err = js_create_arraybuffer(env, sizeof(bare_abstract_server_t), (void **) &server, &handle);
  assert(err == 0);

  uv_loop_t *loop;
  err = js_get_env_loop(env, &loop);
  assert(err == 0);

  server->fd = fd;
  server->env = env;
  server->closing = false;

  err = uv_poll_init(loop, &server->poll, fd);

  if (err < 0) {
    close(fd);
    err = js_throw_error(env, uv_err_name(err), uv_strerror(err));
    assert(err == 0);
    return NULL;
  }

  server->poll.data = server;

  err = js_create_reference(env, argv[2], 1, &server->ctx);
  assert(err == 0);

  err = js_create_reference(env, argv[3], 1, &server->on_connection);
  assert(err == 0);

  err = js_add_deferred_teardown_callback(env, bare_abstract__teardown, (void *) server, &server->teardown);
  assert(err == 0);

  err = uv_poll_start(&server->poll, UV_READABLE, bare_abstract__on_poll);
  assert(err == 0);

  return handle;
}

static js_value_t *
bare_abstract_close(js_env_t *env, js_callback_info_t *info) {
  int err;

  size_t argc = 1;
  js_value_t *argv[1];

  err = js_get_callback_info(env, info, &argc, argv, NULL, NULL);
  assert(err == 0);

  assert(argc == 1);

  bare_abstract_server_t *server;
  err = js_get_arraybuffer_info(env, argv[0], (void **) &server, NULL);
  assert(err == 0);

  if (server->closing) return NULL;

  server->closing = true;

  if (server->fd >= 0) {
    close(server->fd);
    server->fd = -1;
  }

  uv_close((uv_handle_t *) &server->poll, bare_abstract__on_close);

  return NULL;
}

static js_value_t *
bare_abstract_exports(js_env_t *env, js_value_t *exports) {
  int err;

#define V(name, fn) \
  { \
    js_value_t *val; \
    err = js_create_function(env, name, -1, fn, NULL, &val); \
    assert(err == 0); \
    err = js_set_named_property(env, exports, name, val); \
    assert(err == 0); \
  }

  V("connect", bare_abstract_connect)
  V("listen", bare_abstract_listen)
  V("close", bare_abstract_close)
#undef V

  return exports;
}

BARE_MODULE(bare_abstract_socket, bare_abstract_exports)