+ b64encode_urlsafe(cookie, cookie, 18);
+
+ free(fingerprint);
+
+ /* Construct the invitation file */
+ uint8_t outbuf[4096];
+ packmsg_output_t inv = {outbuf, sizeof(outbuf)};
+
+ packmsg_add_uint32(&inv, MESHLINK_INVITATION_VERSION);
+ packmsg_add_str(&inv, name);
+ packmsg_add_str(&inv, s ? s->name : CORE_MESH);
+ packmsg_add_int32(&inv, DEV_CLASS_UNKNOWN); /* TODO: allow this to be set by inviter? */
+
+ /* TODO: Add several host config files to bootstrap connections.
+ * Note: make sure we only add config files of nodes that are in the core mesh or the same submesh,
+ * and are not blacklisted.
+ */
+ config_t configs[5];
+ memset(configs, 0, sizeof(configs));
+ int count = 0;
+
+ if(config_read(mesh, "current", mesh->self->name, &configs[count], mesh->config_key)) {
+ count++;
+ }
+
+ /* Append host config files to the invitation file */
+ packmsg_add_array(&inv, count);
+
+ for(int i = 0; i < count; i++) {
+ packmsg_add_bin(&inv, configs[i].buf, configs[i].len);
+ config_free(&configs[i]);
+ }
+
+ config_t config = {outbuf, packmsg_output_size(&inv, outbuf)};
+
+ if(!invitation_write(mesh, "current", cookiehash, &config, mesh->config_key)) {
+ logger(mesh, MESHLINK_DEBUG, "Could not create invitation file %s: %s\n", cookiehash, strerror(errno));
+ meshlink_errno = MESHLINK_ESTORAGE;
+ pthread_mutex_unlock(&mesh->mutex);
+ return NULL;
+ }
+
+ // Create an URL from the local address, key hash and cookie
+ char *url;
+ xasprintf(&url, "%s/%s%s", address, hash, cookie);
+ free(address);
+
+ pthread_mutex_unlock(&mesh->mutex);
+ return url;
+}
+
+char *meshlink_invite(meshlink_handle_t *mesh, meshlink_submesh_t *submesh, const char *name) {
+ return meshlink_invite_ex(mesh, submesh, name, 0);
+}
+
+bool meshlink_join(meshlink_handle_t *mesh, const char *invitation) {
+ if(!mesh || !invitation) {
+ meshlink_errno = MESHLINK_EINVAL;
+ return false;
+ }
+
+ join_state_t state = {
+ .mesh = mesh,
+ .sock = -1,
+ };
+
+ ecdsa_t *key = NULL;
+ ecdsa_t *hiskey = NULL;
+
+ //TODO: think of a better name for this variable, or of a different way to tokenize the invitation URL.
+ char copy[strlen(invitation) + 1];
+
+ pthread_mutex_lock(&mesh->mutex);
+
+ //Before doing meshlink_join make sure we are not connected to another mesh
+ if(mesh->threadstarted) {
+ logger(mesh, MESHLINK_ERROR, "Cannot join while started\n");
+ meshlink_errno = MESHLINK_EINVAL;
+ goto exit;
+ }
+
+ // Refuse to join a mesh if we are already part of one. We are part of one if we know at least one other node.
+ if(mesh->nodes->count > 1) {
+ logger(mesh, MESHLINK_ERROR, "Already part of an existing mesh\n");
+ meshlink_errno = MESHLINK_EINVAL;
+ goto exit;
+ }
+
+ strcpy(copy, invitation);
+
+ // Split the invitation URL into a list of hostname/port tuples, a key hash and a cookie.
+
+ char *slash = strchr(copy, '/');
+
+ if(!slash) {
+ goto invalid;
+ }
+
+ *slash++ = 0;
+
+ if(strlen(slash) != 48) {
+ goto invalid;
+ }
+
+ char *address = copy;
+ char *port = NULL;
+
+ if(!b64decode(slash, state.hash, 18) || !b64decode(slash + 24, state.cookie, 18)) {
+ goto invalid;
+ }
+
+ if(mesh->inviter_commits_first) {
+ memcpy(state.cookie + 18, ecdsa_get_public_key(mesh->private_key), 32);
+ }
+
+ // Generate a throw-away key for the invitation.
+ key = ecdsa_generate();
+
+ if(!key) {
+ meshlink_errno = MESHLINK_EINTERNAL;
+ goto exit;
+ }
+
+ char *b64key = ecdsa_get_base64_public_key(key);
+ char *comma;
+
+ while(address && *address) {
+ // We allow commas in the address part to support multiple addresses in one invitation URL.
+ comma = strchr(address, ',');
+
+ if(comma) {
+ *comma++ = 0;
+ }
+
+ // Split of the port
+ port = strrchr(address, ':');
+
+ if(!port) {
+ goto invalid;
+ }
+
+ *port++ = 0;
+
+ // IPv6 address are enclosed in brackets, per RFC 3986
+ if(*address == '[') {
+ address++;
+ char *bracket = strchr(address, ']');
+
+ if(!bracket) {
+ goto invalid;
+ }
+
+ *bracket++ = 0;
+
+ if(*bracket) {
+ goto invalid;
+ }
+ }
+
+ // Connect to the meshlink daemon mentioned in the URL.
+ struct addrinfo *ai = str2addrinfo(address, port, SOCK_STREAM);
+
+ if(ai) {
+ for(struct addrinfo *aip = ai; aip; aip = aip->ai_next) {
+ state.sock = socket_in_netns(aip->ai_family, aip->ai_socktype, aip->ai_protocol, mesh->netns);
+
+ if(state.sock == -1) {
+ logger(mesh, MESHLINK_DEBUG, "Could not open socket: %s\n", strerror(errno));
+ meshlink_errno = MESHLINK_ENETWORK;
+ continue;
+ }
+
+ set_timeout(state.sock, 5000);
+
+ if(connect(state.sock, aip->ai_addr, aip->ai_addrlen)) {
+ logger(mesh, MESHLINK_DEBUG, "Could not connect to %s port %s: %s\n", address, port, strerror(errno));
+ meshlink_errno = MESHLINK_ENETWORK;
+ closesocket(state.sock);
+ state.sock = -1;
+ continue;
+ }
+ }
+
+ freeaddrinfo(ai);
+ } else {
+ meshlink_errno = MESHLINK_ERESOLV;
+ }
+
+ if(state.sock != -1 || !comma) {
+ break;
+ }
+
+ address = comma;
+ }
+
+ if(state.sock == -1) {
+ goto exit;
+ }
+
+ logger(mesh, MESHLINK_DEBUG, "Connected to %s port %s...\n", address, port);
+
+ // Tell him we have an invitation, and give him our throw-away key.
+
+ state.blen = 0;
+
+ if(!sendline(state.sock, "0 ?%s %d.%d %s", b64key, PROT_MAJOR, PROT_MINOR, mesh->appname)) {
+ logger(mesh, MESHLINK_DEBUG, "Error sending request to %s port %s: %s\n", address, port, strerror(errno));
+ meshlink_errno = MESHLINK_ENETWORK;
+ goto exit;
+ }
+
+ free(b64key);
+
+ char hisname[4096] = "";
+ int code, hismajor, hisminor = 0;
+
+ if(!recvline(&state) || sscanf(state.line, "%d %s %d.%d", &code, hisname, &hismajor, &hisminor) < 3 || code != 0 || hismajor != PROT_MAJOR || !check_id(hisname) || !recvline(&state) || !rstrip(state.line) || sscanf(state.line, "%d ", &code) != 1 || code != ACK || strlen(state.line) < 3) {
+ logger(mesh, MESHLINK_DEBUG, "Cannot read greeting from peer\n");
+ meshlink_errno = MESHLINK_ENETWORK;
+ goto exit;
+ }
+
+ // Check if the hash of the key he gave us matches the hash in the URL.
+ char *fingerprint = state.line + 2;
+ char hishash[64];
+
+ if(sha512(fingerprint, strlen(fingerprint), hishash)) {
+ logger(mesh, MESHLINK_DEBUG, "Could not create hash\n%s\n", state.line + 2);
+ meshlink_errno = MESHLINK_EINTERNAL;
+ goto exit;
+ }
+
+ if(memcmp(hishash, state.hash, 18)) {
+ logger(mesh, MESHLINK_DEBUG, "Peer has an invalid key!\n%s\n", state.line + 2);
+ meshlink_errno = MESHLINK_EPEER;
+ goto exit;
+ }
+
+ hiskey = ecdsa_set_base64_public_key(fingerprint);
+
+ if(!hiskey) {
+ meshlink_errno = MESHLINK_EINTERNAL;
+ goto exit;
+ }
+
+ // Start an SPTPS session
+ if(!sptps_start(&state.sptps, &state, true, false, key, hiskey, meshlink_invitation_label, sizeof(meshlink_invitation_label), invitation_send, invitation_receive)) {
+ meshlink_errno = MESHLINK_EINTERNAL;
+ goto exit;
+ }
+
+ // Feed rest of input buffer to SPTPS
+ if(!sptps_receive_data(&state.sptps, state.buffer, state.blen)) {
+ meshlink_errno = MESHLINK_EPEER;
+ goto exit;
+ }
+
+ ssize_t len;
+ logger(mesh, MESHLINK_DEBUG, "Starting invitation recv loop: %d %zu\n", state.sock, sizeof(state.line));
+
+ while((len = recv(state.sock, state.line, sizeof(state.line), 0))) {
+ if(len < 0) {
+ if(errno == EINTR) {
+ continue;
+ }
+
+ logger(mesh, MESHLINK_DEBUG, "Error reading data from %s port %s: %s\n", address, port, strerror(errno));
+ meshlink_errno = MESHLINK_ENETWORK;
+ goto exit;
+ }
+
+ if(!sptps_receive_data(&state.sptps, state.line, len)) {
+ meshlink_errno = MESHLINK_EPEER;
+ goto exit;
+ }
+ }
+
+ if(!state.success) {
+ logger(mesh, MESHLINK_DEBUG, "Connection closed by peer, invitation cancelled.\n");
+ meshlink_errno = MESHLINK_EPEER;
+ goto exit;
+ }
+
+ sptps_stop(&state.sptps);
+ ecdsa_free(hiskey);
+ ecdsa_free(key);
+ closesocket(state.sock);
+
+ pthread_mutex_unlock(&mesh->mutex);
+ return true;
+
+invalid:
+ logger(mesh, MESHLINK_DEBUG, "Invalid invitation URL\n");
+ meshlink_errno = MESHLINK_EINVAL;
+exit:
+ sptps_stop(&state.sptps);
+ ecdsa_free(hiskey);
+ ecdsa_free(key);
+
+ if(state.sock != -1) {
+ closesocket(state.sock);
+ }
+
+ pthread_mutex_unlock(&mesh->mutex);
+ return false;
+}
+
+char *meshlink_export(meshlink_handle_t *mesh) {
+ if(!mesh) {
+ meshlink_errno = MESHLINK_EINVAL;
+ return NULL;
+ }
+
+ // Create a config file on the fly.
+
+ uint8_t buf[4096];
+ packmsg_output_t out = {buf, sizeof(buf)};
+ packmsg_add_uint32(&out, MESHLINK_CONFIG_VERSION);
+ packmsg_add_str(&out, mesh->name);
+ packmsg_add_str(&out, CORE_MESH);
+
+ pthread_mutex_lock(&mesh->mutex);
+
+ packmsg_add_int32(&out, mesh->self->devclass);
+ packmsg_add_bool(&out, mesh->self->status.blacklisted);
+ packmsg_add_bin(&out, ecdsa_get_public_key(mesh->private_key), 32);
+ packmsg_add_str(&out, mesh->self->canonical_address ? mesh->self->canonical_address : "");
+
+ uint32_t count = 0;
+
+ for(uint32_t i = 0; i < MAX_RECENT; i++) {
+ if(mesh->self->recent[i].sa.sa_family) {
+ count++;
+ } else {
+ break;
+ }
+ }
+
+ packmsg_add_array(&out, count);
+
+ for(uint32_t i = 0; i < count; i++) {
+ packmsg_add_sockaddr(&out, &mesh->self->recent[i]);
+ }
+
+ packmsg_add_int64(&out, 0);
+ packmsg_add_int64(&out, 0);
+
+ pthread_mutex_unlock(&mesh->mutex);
+
+ if(!packmsg_output_ok(&out)) {
+ logger(mesh, MESHLINK_DEBUG, "Error creating export data\n");
+ meshlink_errno = MESHLINK_EINTERNAL;
+ return NULL;
+ }
+
+ // Prepare a base64-encoded packmsg array containing our config file
+
+ uint32_t len = packmsg_output_size(&out, buf);
+ uint32_t len2 = ((len + 4) * 4) / 3 + 4;
+ uint8_t *buf2 = xmalloc(len2);
+ packmsg_output_t out2 = {buf2, len2};
+ packmsg_add_array(&out2, 1);
+ packmsg_add_bin(&out2, buf, packmsg_output_size(&out, buf));
+
+ if(!packmsg_output_ok(&out2)) {
+ logger(mesh, MESHLINK_DEBUG, "Error creating export data\n");
+ meshlink_errno = MESHLINK_EINTERNAL;
+ free(buf2);
+ return NULL;
+ }
+
+ b64encode_urlsafe(buf2, (char *)buf2, packmsg_output_size(&out2, buf2));
+
+ return (char *)buf2;
+}
+
+bool meshlink_import(meshlink_handle_t *mesh, const char *data) {
+ if(!mesh || !data) {
+ meshlink_errno = MESHLINK_EINVAL;
+ return false;
+ }
+
+ size_t datalen = strlen(data);
+ uint8_t *buf = xmalloc(datalen);
+ int buflen = b64decode(data, buf, datalen);
+
+ if(!buflen) {
+ logger(mesh, MESHLINK_DEBUG, "Invalid data\n");
+ meshlink_errno = MESHLINK_EPEER;
+ return false;
+ }
+
+ packmsg_input_t in = {buf, buflen};
+ uint32_t count = packmsg_get_array(&in);
+
+ if(!count) {
+ logger(mesh, MESHLINK_DEBUG, "Invalid data\n");
+ meshlink_errno = MESHLINK_EPEER;
+ return false;
+ }
+
+ pthread_mutex_lock(&mesh->mutex);
+
+ while(count--) {
+ const void *data;
+ uint32_t len = packmsg_get_bin_raw(&in, &data);
+
+ if(!len) {
+ break;
+ }
+
+ packmsg_input_t in2 = {data, len};
+ uint32_t version = packmsg_get_uint32(&in2);
+ char *name = packmsg_get_str_dup(&in2);
+
+ if(!packmsg_input_ok(&in2) || version != MESHLINK_CONFIG_VERSION || !check_id(name)) {
+ free(name);
+ packmsg_input_invalidate(&in);
+ break;
+ }
+
+ if(!check_id(name)) {
+ free(name);
+ break;
+ }
+
+ node_t *n = lookup_node(mesh, name);
+
+ if(n) {
+ logger(mesh, MESHLINK_DEBUG, "Node %s already exists, not importing\n", name);
+ free(name);
+ continue;
+ }
+
+ n = new_node();
+ n->name = name;
+
+ config_t config = {data, len};
+
+ if(!node_read_from_config(mesh, n, &config)) {
+ free_node(n);
+ packmsg_input_invalidate(&in);
+ break;
+ }
+
+ /* Clear the reachability times, since we ourself have never seen these nodes yet */
+ n->last_reachable = 0;
+ n->last_unreachable = 0;
+
+ if(!node_write_config(mesh, n)) {
+ free_node(n);
+ return false;
+ }
+
+ node_add(mesh, n);
+ }
+
+ pthread_mutex_unlock(&mesh->mutex);
+
+ free(buf);
+
+ if(!packmsg_done(&in)) {
+ logger(mesh, MESHLINK_ERROR, "Invalid data\n");
+ meshlink_errno = MESHLINK_EPEER;
+ return false;
+ }
+
+ if(!config_sync(mesh, "current")) {
+ return false;
+ }
+
+ return true;
+}
+
+static bool blacklist(meshlink_handle_t *mesh, node_t *n) {
+ if(n == mesh->self) {
+ logger(mesh, MESHLINK_ERROR, "%s blacklisting itself?\n", n->name);
+ meshlink_errno = MESHLINK_EINVAL;
+ return false;
+ }
+
+ if(n->status.blacklisted) {
+ logger(mesh, MESHLINK_DEBUG, "Node %s already blacklisted\n", n->name);
+ return true;
+ }
+
+ n->status.blacklisted = true;
+
+ /* Immediately shut down any connections we have with the blacklisted node.
+ * We can't call terminate_connection(), because we might be called from a callback function.
+ */
+ for list_each(connection_t, c, mesh->connections) {
+ if(c->node == n) {
+ shutdown(c->socket, SHUT_RDWR);
+ }
+ }
+
+ utcp_abort_all_connections(n->utcp);
+
+ n->mtu = 0;
+ n->minmtu = 0;
+ n->maxmtu = MTU;
+ n->mtuprobes = 0;
+ n->status.udp_confirmed = false;
+
+ if(n->status.reachable) {
+ n->last_unreachable = mesh->loop.now.tv_sec;
+ }
+
+ /* Graph updates will suppress status updates for blacklisted nodes, so we need to
+ * manually call the status callback if necessary.
+ */
+ if(n->status.reachable && mesh->node_status_cb) {
+ mesh->node_status_cb(mesh, (meshlink_node_t *)n, false);
+ }
+
+ return node_write_config(mesh, n) && config_sync(mesh, "current");
+}
+
+bool meshlink_blacklist(meshlink_handle_t *mesh, meshlink_node_t *node) {
+ if(!mesh || !node) {
+ meshlink_errno = MESHLINK_EINVAL;
+ return false;
+ }
+
+ pthread_mutex_lock(&mesh->mutex);
+
+ if(!blacklist(mesh, (node_t *)node)) {
+ pthread_mutex_unlock(&mesh->mutex);
+ return false;
+ }
+
+ pthread_mutex_unlock(&mesh->mutex);
+
+ logger(mesh, MESHLINK_DEBUG, "Blacklisted %s.\n", node->name);
+ return true;
+}
+
+bool meshlink_blacklist_by_name(meshlink_handle_t *mesh, const char *name) {
+ if(!mesh || !name) {
+ meshlink_errno = MESHLINK_EINVAL;
+ return false;
+ }
+
+ pthread_mutex_lock(&mesh->mutex);
+
+ node_t *n = lookup_node(mesh, (char *)name);
+
+ if(!n) {
+ n = new_node();
+ n->name = xstrdup(name);
+ node_add(mesh, n);
+ }
+
+ if(!blacklist(mesh, (node_t *)n)) {
+ pthread_mutex_unlock(&mesh->mutex);
+ return false;
+ }
+
+ pthread_mutex_unlock(&mesh->mutex);
+
+ logger(mesh, MESHLINK_DEBUG, "Blacklisted %s.\n", name);
+ return true;
+}
+
+static bool whitelist(meshlink_handle_t *mesh, node_t *n) {
+ if(n == mesh->self) {
+ logger(mesh, MESHLINK_ERROR, "%s whitelisting itself?\n", n->name);
+ meshlink_errno = MESHLINK_EINVAL;
+ return false;
+ }
+
+ if(!n->status.blacklisted) {
+ logger(mesh, MESHLINK_DEBUG, "Node %s was already whitelisted\n", n->name);
+ return true;
+ }
+
+ n->status.blacklisted = false;
+
+ if(n->status.reachable) {
+ n->last_reachable = mesh->loop.now.tv_sec;
+ update_node_status(mesh, n);
+ }
+
+ return node_write_config(mesh, n) && config_sync(mesh, "current");
+}
+
+bool meshlink_whitelist(meshlink_handle_t *mesh, meshlink_node_t *node) {
+ if(!mesh || !node) {
+ meshlink_errno = MESHLINK_EINVAL;
+ return false;
+ }
+
+ pthread_mutex_lock(&mesh->mutex);
+
+ if(!whitelist(mesh, (node_t *)node)) {
+ pthread_mutex_unlock(&mesh->mutex);
+ return false;
+ }
+
+ pthread_mutex_unlock(&mesh->mutex);
+
+ logger(mesh, MESHLINK_DEBUG, "Whitelisted %s.\n", node->name);
+ return true;
+}
+
+bool meshlink_whitelist_by_name(meshlink_handle_t *mesh, const char *name) {
+ if(!mesh || !name) {
+ meshlink_errno = MESHLINK_EINVAL;
+ return false;
+ }
+
+ pthread_mutex_lock(&mesh->mutex);
+
+ node_t *n = lookup_node(mesh, (char *)name);
+
+ if(!n) {
+ n = new_node();
+ n->name = xstrdup(name);
+ node_add(mesh, n);
+ }
+
+ if(!whitelist(mesh, (node_t *)n)) {
+ pthread_mutex_unlock(&mesh->mutex);
+ return false;
+ }
+
+ pthread_mutex_unlock(&mesh->mutex);
+
+ logger(mesh, MESHLINK_DEBUG, "Whitelisted %s.\n", name);
+ return true;
+}
+
+void meshlink_set_default_blacklist(meshlink_handle_t *mesh, bool blacklist) {
+ mesh->default_blacklist = blacklist;
+}
+
+bool meshlink_forget_node(meshlink_handle_t *mesh, meshlink_node_t *node) {
+ if(!mesh || !node) {
+ meshlink_errno = MESHLINK_EINVAL;
+ return false;
+ }
+
+ node_t *n = (node_t *)node;
+
+ pthread_mutex_lock(&mesh->mutex);
+
+ /* Check that the node is not reachable */
+ if(n->status.reachable || n->connection) {
+ pthread_mutex_unlock(&mesh->mutex);
+ logger(mesh, MESHLINK_WARNING, "Could not forget %s: still reachable", n->name);
+ return false;
+ }
+
+ /* Check that we don't have any active UTCP connections */
+ if(n->utcp && utcp_is_active(n->utcp)) {
+ pthread_mutex_unlock(&mesh->mutex);
+ logger(mesh, MESHLINK_WARNING, "Could not forget %s: active UTCP connections", n->name);
+ return false;
+ }
+
+ /* Check that we have no active connections to this node */
+ for list_each(connection_t, c, mesh->connections) {
+ if(c->node == n) {
+ pthread_mutex_unlock(&mesh->mutex);
+ logger(mesh, MESHLINK_WARNING, "Could not forget %s: active connection", n->name);
+ return false;
+ }
+ }
+
+ /* Remove any pending outgoings to this node */
+ if(mesh->outgoings) {
+ for list_each(outgoing_t, outgoing, mesh->outgoings) {
+ if(outgoing->node == n) {
+ list_delete_node(mesh->outgoings, node);
+ }
+ }
+ }
+
+ /* Delete the config file for this node */
+ if(!config_delete(mesh, "current", n->name)) {
+ pthread_mutex_unlock(&mesh->mutex);
+ return false;
+ }
+
+ /* Delete the node struct and any remaining edges referencing this node */
+ node_del(mesh, n);
+
+ pthread_mutex_unlock(&mesh->mutex);
+
+ return config_sync(mesh, "current");
+}
+
+/* Hint that a hostname may be found at an address
+ * See header file for detailed comment.
+ */
+void meshlink_hint_address(meshlink_handle_t *mesh, meshlink_node_t *node, const struct sockaddr *addr) {
+ if(!mesh || !node || !addr) {
+ meshlink_errno = EINVAL;
+ return;
+ }
+
+ pthread_mutex_lock(&mesh->mutex);
+
+ node_t *n = (node_t *)node;
+
+ if(node_add_recent_address(mesh, n, (sockaddr_t *)addr)) {
+ if(!node_write_config(mesh, n)) {
+ logger(mesh, MESHLINK_DEBUG, "Could not update %s\n", n->name);
+ }
+ }
+
+ pthread_mutex_unlock(&mesh->mutex);
+ // @TODO do we want to fire off a connection attempt right away?
+}
+
+static bool channel_pre_accept(struct utcp *utcp, uint16_t port) {
+ (void)port;
+ node_t *n = utcp->priv;
+ meshlink_handle_t *mesh = n->mesh;
+ return mesh->channel_accept_cb;
+}
+
+static void aio_signal(meshlink_handle_t *mesh, meshlink_channel_t *channel, meshlink_aio_buffer_t *aio) {
+ if(aio->data) {
+ if(aio->cb.buffer) {
+ aio->cb.buffer(mesh, channel, aio->data, aio->len, aio->priv);
+ }
+ } else {
+ if(aio->cb.fd) {
+ aio->cb.fd(mesh, channel, aio->fd, aio->done, aio->priv);
+ }
+ }
+}
+
+static ssize_t channel_recv(struct utcp_connection *connection, const void *data, size_t len) {
+ meshlink_channel_t *channel = connection->priv;