OverviewFilesCommitsIssuesDocsReleasesPeers

autobase-doctor / node_modules / bare-tls / binding.c

662 lines · c ref:
#include <assert.h>
#include <bare.h>
#include <js.h>
#include <openssl/base.h>
#include <openssl/bio.h>
#include <openssl/err.h>
#include <openssl/evp.h>
#include <openssl/ssl.h>
#include <stddef.h>

typedef struct {
  SSL_CTX *ssl;
  BIO_METHOD *io;

  js_env_t *env;
  js_ref_t *ctx;
} bare_tls_context_t;

typedef struct {
  SSL *ssl;
  BIO *io;
  X509 *certificate;
  EVP_PKEY *key;

  uint8_t *alpn;
  size_t alpn_len;

  js_env_t *env;
  js_ref_t *ctx;
  js_ref_t *on_read;
  js_ref_t *on_write;
} bare_tls_t;

static int
bare_tls__on_read(BIO *io, char *buffer, int len) {
  if (len == 0) return 0;

  int err;

  BIO_clear_retry_flags(io);

  bare_tls_t *socket = BIO_get_ex_data(io, 0);

  js_env_t *env = socket->env;

  js_value_t *arraybuffer;
  err = js_create_external_arraybuffer(env, (void *) buffer, len, NULL, NULL, &arraybuffer);
  assert(err == 0);

  js_value_t *typedarray;
  err = js_create_typedarray(env, js_uint8array, len, arraybuffer, 0, &typedarray);
  assert(err == 0);

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

  js_value_t *on_read;
  err = js_get_reference_value(env, socket->on_read, &on_read);
  assert(err == 0);

  js_value_t *result;
  err = js_call_function(env, ctx, on_read, 1, &typedarray, &result);
  if (err < 0) return -1;

  err = js_get_value_int32(env, result, &len);
  assert(err == 0);

  err = js_detach_arraybuffer(env, arraybuffer);
  assert(err == 0);

  if (len == 0) {
    BIO_set_retry_read(io);

    return -1;
  }

  return len;
}

static int
bare_tls__on_write(BIO *io, const char *buffer, int len) {
  if (len == 0) return 0;

  int err;

  BIO_clear_retry_flags(io);

  bare_tls_t *socket = BIO_get_ex_data(io, 0);

  js_env_t *env = socket->env;

  js_value_t *arraybuffer;
  err = js_create_external_arraybuffer(env, (void *) buffer, len, NULL, NULL, &arraybuffer);
  assert(err == 0);

  js_value_t *typedarray;
  err = js_create_typedarray(env, js_uint8array, len, arraybuffer, 0, &typedarray);
  assert(err == 0);

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

  js_value_t *on_write;
  err = js_get_reference_value(env, socket->on_write, &on_write);
  assert(err == 0);

  js_value_t *result;
  err = js_call_function(env, ctx, on_write, 1, &typedarray, &result);
  if (err < 0) return -1;

  err = js_get_value_int32(env, result, &len);
  assert(err == 0);

  err = js_detach_arraybuffer(env, arraybuffer);
  assert(err == 0);

  if (len == 0) {
    BIO_set_retry_write(io);

    return -1;
  }

  return len;
}

static long
bare_tls__on_ctrl(BIO *io, int cmd, long argc, void *argv) {
  switch (cmd) {
  case BIO_CTRL_FLUSH:
    return 1;

  default:
    return 0;
  }
}

static int
bare_tls__on_alpn_select(SSL *ssl, const uint8_t **out, uint8_t *outlen, const uint8_t *in, unsigned int inlen, void *arg) {
  bare_tls_t *socket = (bare_tls_t *) SSL_get_ex_data(ssl, 0);

  if (socket == NULL || socket->alpn == NULL || socket->alpn_len == 0) {
    return SSL_TLSEXT_ERR_NOACK;
  }

  if (SSL_select_next_proto((uint8_t **) out, outlen, socket->alpn, (unsigned int) socket->alpn_len, in, inlen) != OPENSSL_NPN_NEGOTIATED) {
    return SSL_TLSEXT_ERR_NOACK;
  }

  return SSL_TLSEXT_ERR_OK;
}

static void
bare_tls__on_teardown(void *data) {
  int err;

  bare_tls_context_t *context = (bare_tls_context_t *) data;

  js_env_t *env = context->env;

  SSL_CTX_free(context->ssl);

  BIO_meth_free(context->io);

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

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

  js_value_t *handle;

  bare_tls_context_t *context;
  err = js_create_arraybuffer(env, sizeof(bare_tls_context_t), (void **) &context, &handle);
  assert(err == 0);

  BIO_METHOD *io = context->io = BIO_meth_new(BIO_get_new_index() | BIO_TYPE_SOURCE_SINK, "callback");

  if (io == NULL) goto err;

  SSL_CTX *ssl = context->ssl = SSL_CTX_new(TLS_method());

  if (ssl == NULL) {
    BIO_meth_free(io);

    goto err;
  }

  BIO_meth_set_read(io, bare_tls__on_read);
  BIO_meth_set_write(io, bare_tls__on_write);
  BIO_meth_set_ctrl(io, bare_tls__on_ctrl);

  err = SSL_CTX_set_ex_data(ssl, 0, (void *) context);
  assert(err == 1);

  err = SSL_CTX_set_min_proto_version(ssl, TLS1_2_VERSION);
  assert(err == 1);

  SSL_CTX_set_alpn_select_cb(ssl, bare_tls__on_alpn_select, NULL);

  context->env = env;

  err = js_add_teardown_callback(env, bare_tls__on_teardown, (void *) context);
  assert(err == 0);

  err = js_create_reference(env, handle, 1, &context->ctx);
  assert(err == 0);

  return handle;

err:
  js_throw_error(env, ERR_reason_symbol_name(ERR_peek_last_error()), "Context initialisation failed");
  return NULL;
}

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

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

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

  assert(argc == 9);

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

  js_value_t *handle;

  bare_tls_t *socket;
  err = js_create_arraybuffer(env, sizeof(bare_tls_t), (void **) &socket, &handle);
  assert(err == 0);

  socket->certificate = NULL;
  socket->key = NULL;
  socket->alpn = NULL;
  socket->alpn_len = 0;

  BIO *io = socket->io = BIO_new(context->io);

  if (io == NULL) goto err;

  SSL *ssl = socket->ssl = SSL_new(context->ssl);

  if (ssl == NULL) {
    BIO_free(io);

    goto err;
  }

  err = BIO_set_ex_data(io, 0, (void *) socket);
  assert(err == 1);

  BIO_set_init(io, true);

  err = SSL_set_ex_data(ssl, 0, (void *) socket);
  assert(err == 1);

  SSL_set_bio(ssl, io, io);

  bool is_server;
  err = js_get_value_bool(env, argv[1], &is_server);
  assert(err == 0);

  if (is_server) SSL_set_accept_state(ssl);
  else SSL_set_connect_state(ssl);

  bool has_cert;
  err = js_is_typedarray(env, argv[2], &has_cert);
  assert(err == 0);

  if (has_cert) {
    char *pem;
    size_t len;
    err = js_get_typedarray_info(env, argv[2], NULL, (void **) &pem, &len, NULL, NULL);
    assert(err == 0);

    BIO *io = BIO_new(BIO_s_mem());
    BIO_write(io, pem, (int) len);

    X509 *certificate = socket->certificate = PEM_read_bio_X509(io, NULL, NULL, NULL);

    BIO_free(io);

    if (certificate == NULL) {
      SSL_free(ssl);

      goto err;
    }

    err = SSL_use_certificate(ssl, certificate);

    if (err == 0) {
      SSL_free(ssl);

      X509_free(certificate);

      goto err;
    }
  }

  bool has_key;
  err = js_is_typedarray(env, argv[3], &has_key);
  assert(err == 0);

  if (has_key) {
    char *pem;
    size_t len;
    err = js_get_typedarray_info(env, argv[3], NULL, (void **) &pem, &len, NULL, NULL);
    assert(err == 0);

    BIO *io = BIO_new(BIO_s_mem());
    BIO_write(io, pem, (int) len);

    EVP_PKEY *key = socket->key = PEM_read_bio_PrivateKey(io, NULL, NULL, NULL);

    BIO_free(io);

    if (key == NULL) {
      SSL_free(ssl);

      goto err;
    }

    int res = SSL_use_PrivateKey(ssl, key);

    if (res == 0) {
      SSL_free(ssl);

      EVP_PKEY_free(key);

      goto err;
    }
  }

  bool has_host;
  err = js_is_string(env, argv[4], &has_host);
  assert(err == 0);

  if (has_host) {
    size_t len;
    err = js_get_value_string_utf8(env, argv[4], NULL, 0, &len);
    assert(err == 0);

    len += 1 /* NULL */;

    utf8_t *host = malloc(len);
    err = js_get_value_string_utf8(env, argv[4], host, len, NULL);
    assert(err == 0);

    err = SSL_set_tlsext_host_name(ssl, (char *) host);
    assert(err == 1);

    free(host);
  }

  bool has_alpn;
  err = js_is_typedarray(env, argv[5], &has_alpn);
  assert(err == 0);

  if (has_alpn) {
    uint8_t *alpn;
    size_t len;
    err = js_get_typedarray_info(env, argv[5], NULL, (void **) &alpn, &len, NULL, NULL);
    assert(err == 0);

    if (is_server) {
      socket->alpn = malloc(len);
      socket->alpn_len = len;

      memcpy(socket->alpn, alpn, len);
    } else {
      err = SSL_set_alpn_protos(ssl, alpn, len);
      assert(err == 0);
    }
  }

  socket->env = env;

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

  err = js_create_reference(env, argv[7], 1, &socket->on_read);
  assert(err == 0);

  err = js_create_reference(env, argv[8], 1, &socket->on_write);
  assert(err == 0);

  return handle;

err:
  js_throw_error(env, ERR_reason_symbol_name(ERR_peek_last_error()), "Socket initialisation failed");
  return NULL;
}

static js_value_t *
bare_tls_destroy(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_tls_t *socket;
  err = js_get_arraybuffer_info(env, argv[0], (void **) &socket, NULL);
  assert(err == 0);

  SSL_free(socket->ssl);

  if (socket->certificate) X509_free(socket->certificate);

  if (socket->key) EVP_PKEY_free(socket->key);

  if (socket->alpn) free(socket->alpn);

  err = js_delete_reference(env, socket->on_read);
  assert(err == 0);

  err = js_delete_reference(env, socket->on_write);
  assert(err == 0);

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

  return NULL;
}

static js_value_t *
bare_tls_handshake(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_tls_t *socket;
  err = js_get_arraybuffer_info(env, argv[0], (void **) &socket, NULL);
  assert(err == 0);

  bool done = true;

  err = SSL_do_handshake(socket->ssl);

  if (err <= 0) {
    err = SSL_get_error(socket->ssl, err);

    if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) {
      done = false;
    } else {
      js_throw_error(env, ERR_reason_symbol_name(ERR_peek_last_error()), "Handshake failed");
      return NULL;
    }
  }

  js_value_t *result;
  err = js_get_boolean(env, done, &result);
  assert(err == 0);

  return result;
}

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

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

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

  assert(argc == 2);

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

  char *buffer;
  size_t len;
  err = js_get_typedarray_info(env, argv[1], NULL, (void **) &buffer, &len, NULL, NULL);
  assert(err == 0);

  bool retry = false;
  bool eof = false;

  err = SSL_read(socket->ssl, buffer, len);

  if (err <= 0) {
    err = SSL_get_error(socket->ssl, err);

    if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) {
      retry = true;
    } else if (SSL_get_shutdown(socket->ssl)) {
      eof = true;
    } else {
      js_throw_error(env, ERR_reason_symbol_name(ERR_peek_last_error()), "Read failed");
      return NULL;
    }
  }

  int res = eof ? 0 : retry ? -1
                            : err;

  js_value_t *result;
  err = js_create_int64(env, res, &result);
  assert(err == 0);

  return result;
}

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

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

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

  assert(argc == 2);

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

  char *buffer;
  size_t len;
  err = js_get_typedarray_info(env, argv[1], NULL, (void **) &buffer, &len, NULL, NULL);
  assert(err == 0);

  bool retry = false;

  err = SSL_write(socket->ssl, buffer, len);

  if (err <= 0) {
    err = SSL_get_error(socket->ssl, err);

    if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) {
      retry = true;
    } else {
      js_throw_error(env, ERR_reason_symbol_name(ERR_peek_last_error()), "Write failed");
      return NULL;
    }
  }

  int res = retry ? 0 : err;

  js_value_t *result;
  err = js_create_int64(env, res, &result);
  assert(err == 0);

  return result;
}

static js_value_t *
bare_tls_shutdown(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_tls_t *socket;
  err = js_get_arraybuffer_info(env, argv[0], (void **) &socket, NULL);
  assert(err == 0);

  err = SSL_shutdown(socket->ssl);

  if (err < 0) {
    js_throw_error(env, ERR_reason_symbol_name(ERR_peek_last_error()), "Shutdown failed");
    return NULL;
  }

  js_value_t *result;
  err = js_get_boolean(env, err == 1, &result);
  assert(err == 0);

  return result;
}

static js_value_t *
bare_tls_alpn_protocol(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_tls_t *socket;
  err = js_get_arraybuffer_info(env, argv[0], (void **) &socket, NULL);
  assert(err == 0);

  const uint8_t *data = NULL;
  unsigned int len = 0;

  SSL_get0_alpn_selected(socket->ssl, &data, &len);

  js_value_t *result;

  if (data && len) {
    err = js_create_string_utf8(env, data, len, &result);
    assert(err == 0);
  } else {
    err = js_get_null(env, &result);
    assert(err == 0);
  }

  return result;
}

static js_value_t *
bare_tls_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("context", bare_tls_context);
  V("init", bare_tls_init);
  V("destroy", bare_tls_destroy);
  V("handshake", bare_tls_handshake);
  V("read", bare_tls_read);
  V("write", bare_tls_write);
  V("shutdown", bare_tls_shutdown);
  V("alpnProtocol", bare_tls_alpn_protocol);
#undef V

  return exports;
}

BARE_MODULE(bare_tls, bare_tls_exports)