From: Guus Sliepen Date: Wed, 19 Dec 2018 20:26:36 +0000 (+0100) Subject: Add support for opening a MeshLink instance without permanent storage. X-Git-Url: https://git.meshlink.io/?a=commitdiff_plain;h=706d051e806defed9e168dcab0c8ced2e58d8dd1;p=meshlink Add support for opening a MeshLink instance without permanent storage. --- diff --git a/src/conf.c b/src/conf.c index 9e93c9a1..9664e2b7 100644 --- a/src/conf.c +++ b/src/conf.c @@ -75,6 +75,10 @@ static void deltree(const char *dirname) { /// Create a fresh configuration directory bool config_init(meshlink_handle_t *mesh) { + if(!mesh->confbase) { + return true; + } + if(mkdir(mesh->confbase, 0700) && errno != EEXIST) { logger(mesh, MESHLINK_DEBUG, "Could not create directory %s: %s\n", mesh->confbase, strerror(errno)); return false; @@ -131,6 +135,10 @@ bool config_destroy(const char *confbase) { /// Check the presence of the main configuration file. bool main_config_exists(meshlink_handle_t *mesh) { + if(!mesh->confbase) { + return false; + } + char path[PATH_MAX]; make_main_path(mesh, path, sizeof(path)); @@ -139,6 +147,10 @@ bool main_config_exists(meshlink_handle_t *mesh) { /// Lock the main configuration file. bool main_config_lock(meshlink_handle_t *mesh) { + if(!mesh->confbase) { + return true; + } + char path[PATH_MAX]; make_main_path(mesh, path, sizeof(path)); @@ -181,6 +193,10 @@ void main_config_unlock(meshlink_handle_t *mesh) { /// Read a configuration file from a FILE handle. bool config_read_file(meshlink_handle_t *mesh, FILE *f, config_t *config) { + if(!mesh->confbase) { + return false; + } + (void)mesh; long len; @@ -228,6 +244,10 @@ bool config_read_file(meshlink_handle_t *mesh, FILE *f, config_t *config) { /// Write a configuration file to a FILE handle. bool config_write_file(meshlink_handle_t *mesh, FILE *f, const config_t *config) { + if(!mesh->confbase) { + return true; + } + if(mesh->config_key) { uint8_t buf[config->len + 16]; size_t len = sizeof(buf); @@ -266,6 +286,10 @@ void config_free(config_t *config) { /// Check the presence of a host configuration file. bool config_exists(meshlink_handle_t *mesh, const char *name) { + if(!mesh->confbase) { + return false; + } + char path[PATH_MAX]; make_host_path(mesh, name, path, sizeof(path)); @@ -274,6 +298,10 @@ bool config_exists(meshlink_handle_t *mesh, const char *name) { /// Read a host configuration file. bool config_read(meshlink_handle_t *mesh, const char *name, config_t *config) { + if(!mesh->confbase) { + return false; + } + char path[PATH_MAX]; make_host_path(mesh, name, path, sizeof(path)); @@ -294,8 +322,37 @@ bool config_read(meshlink_handle_t *mesh, const char *name, config_t *config) { return true; } +void config_scan_all(meshlink_handle_t *mesh, config_scan_action_t action) { + if(!mesh->confbase) { + return; + } + + DIR *dir; + struct dirent *ent; + char dname[PATH_MAX]; + make_host_path(mesh, NULL, dname, sizeof(dname)); + + dir = opendir(dname); + + if(!dir) { + logger(mesh, MESHLINK_ERROR, "Could not open %s: %s", dname, strerror(errno)); + meshlink_errno = MESHLINK_ESTORAGE; + return; + } + + while((ent = readdir(dir))) { + action(mesh, ent->d_name); + } + + closedir(dir); +} + /// Write a host configuration file. bool config_write(meshlink_handle_t *mesh, const char *name, const config_t *config) { + if(!mesh->confbase) { + return true; + } + char path[PATH_MAX]; make_host_path(mesh, name, path, sizeof(path)); @@ -318,6 +375,10 @@ bool config_write(meshlink_handle_t *mesh, const char *name, const config_t *con /// Read the main configuration file. bool main_config_read(meshlink_handle_t *mesh, config_t *config) { + if(!mesh->confbase) { + return false; + } + char path[PATH_MAX]; make_main_path(mesh, path, sizeof(path)); @@ -340,6 +401,10 @@ bool main_config_read(meshlink_handle_t *mesh, config_t *config) { /// Write the main configuration file. bool main_config_write(meshlink_handle_t *mesh, const config_t *config) { + if(!mesh->confbase) { + return true; + } + char path[PATH_MAX]; make_main_path(mesh, path, sizeof(path)); @@ -362,6 +427,10 @@ bool main_config_write(meshlink_handle_t *mesh, const config_t *config) { /// Read an invitation file, and immediately delete it. bool invitation_read(meshlink_handle_t *mesh, const char *name, config_t *config) { + if(!mesh->confbase) { + return false; + } + char path[PATH_MAX]; char used_path[PATH_MAX]; make_invitation_path(mesh, name, path, sizeof(path)); @@ -416,6 +485,10 @@ bool invitation_read(meshlink_handle_t *mesh, const char *name, config_t *config /// Write an invitation file. bool invitation_write(meshlink_handle_t *mesh, const char *name, const config_t *config) { + if(!mesh->confbase) { + return true; + } + char path[PATH_MAX]; make_invitation_path(mesh, name, path, sizeof(path)); @@ -438,6 +511,10 @@ bool invitation_write(meshlink_handle_t *mesh, const char *name, const config_t /// Purge old invitation files size_t invitation_purge_old(meshlink_handle_t *mesh, time_t deadline) { + if(!mesh->confbase) { + return true; + } + char path[PATH_MAX]; make_invitation_path(mesh, "", path, sizeof(path)); diff --git a/src/conf.h b/src/conf.h index f2bb0762..63d5fef4 100644 --- a/src/conf.h +++ b/src/conf.h @@ -27,6 +27,8 @@ typedef struct config_t { size_t len; } config_t; +typedef void (*config_scan_action_t)(struct meshlink_handle *mesh, const char *name); + //extern bool config_read_file(struct meshlink_handle *mesh, FILE *f, struct config_t *); //extern bool config_write_file(struct meshlink_handle *mesh, FILE *f, const struct config_t *); extern void config_free(struct config_t *config); @@ -43,6 +45,7 @@ extern bool main_config_write(struct meshlink_handle *mesh, const struct config_ extern bool config_exists(struct meshlink_handle *mesh, const char *name); extern bool config_read(struct meshlink_handle *mesh, const char *name, struct config_t *); extern bool config_write(struct meshlink_handle *mesh, const char *name, const struct config_t *); +extern void config_scan_all(struct meshlink_handle *mesh, config_scan_action_t action); extern bool invitation_read(struct meshlink_handle *mesh, const char *name, struct config_t *); extern bool invitation_write(struct meshlink_handle *mesh, const char *name, const struct config_t *); diff --git a/src/meshlink.c b/src/meshlink.c index a3e3eb7c..f6ebe199 100644 --- a/src/meshlink.c +++ b/src/meshlink.c @@ -558,36 +558,28 @@ static bool finalize_join(meshlink_handle_t *mesh, const void *buf, uint16_t len return false; } - packmsg_input_t in2 = {data, len}; - uint32_t version = packmsg_get_uint32(&in2); + config_t config = {data, len}; + node_t *n = new_node(); - if(version != MESHLINK_CONFIG_VERSION) { + if(!node_read_from_config(mesh, n, &config)) { + free_node(n); logger(mesh, MESHLINK_ERROR, "Invalid host config file in invitation file!\n"); + meshlink_errno = MESHLINK_EPEER; return false; } - char *host = packmsg_get_str_dup(&in2); - - if(!check_id(host)) { - logger(mesh, MESHLINK_ERROR, "Invalid node name in invitation file!\n"); - free(host); - return false; - } - - if(!strcmp(host, name)) { + if(!strcmp(n->name, name)) { logger(mesh, MESHLINK_DEBUG, "Secondary chunk would overwrite our own host config file.\n"); - free(host); + free_node(n); + meshlink_errno = MESHLINK_EPEER; return false; } - config_t config = {data, len}; - config_write(mesh, host, &config); - - node_t *n = new_node(); - n->name = host; - node_read_full(mesh, n); - n->devclass = mesh->devclass; node_add(mesh, n); + + if(!config_write(mesh, n->name, &config)) { + return false; + } } sptps_send_record(&(mesh->sptps), 1, ecdsa_get_public_key(mesh->private_key), 32); @@ -599,8 +591,6 @@ static bool finalize_join(meshlink_handle_t *mesh, const void *buf, uint16_t len logger(mesh, MESHLINK_DEBUG, "Configuration stored in: %s\n", mesh->confbase); - load_all_nodes(mesh); - return true; } @@ -942,18 +932,10 @@ static bool meshlink_read_config(meshlink_handle_t *mesh) { } -meshlink_handle_t *meshlink_open_encrypted(const char *confbase, const char *name, const char *appname, dev_class_t devclass, const void *key, size_t keylen) { +static meshlink_handle_t *meshlink_open_internal(const char *confbase, const char *name, const char *appname, dev_class_t devclass, const void *key, size_t keylen) { // Validate arguments provided by the application bool usingname = false; - logger(NULL, MESHLINK_DEBUG, "meshlink_open called\n"); - - if(!confbase || !*confbase) { - logger(NULL, MESHLINK_ERROR, "No confbase given!\n"); - meshlink_errno = MESHLINK_EINVAL; - return NULL; - } - if(!appname || !*appname) { logger(NULL, MESHLINK_ERROR, "No appname given!\n"); meshlink_errno = MESHLINK_EINVAL; @@ -966,17 +948,14 @@ meshlink_handle_t *meshlink_open_encrypted(const char *confbase, const char *nam return NULL; } - if(!name || !*name) { - logger(NULL, MESHLINK_ERROR, "No name given!\n"); - //return NULL; - } else { //check name only if there is a name != NULL + if(name) { if(!check_id(name)) { logger(NULL, MESHLINK_ERROR, "Invalid name given!\n"); meshlink_errno = MESHLINK_EINVAL; return NULL; - } else { - usingname = true; } + + usingname = true; } if((int)devclass < 0 || devclass > _DEV_CLASS_MAX) { @@ -985,14 +964,12 @@ meshlink_handle_t *meshlink_open_encrypted(const char *confbase, const char *nam return NULL; } - if((key && !keylen) || (!key && keylen)) { - logger(NULL, MESHLINK_ERROR, "Invalid key length!\n"); - meshlink_errno = MESHLINK_EINVAL; - return NULL; + meshlink_handle_t *mesh = xzalloc(sizeof(meshlink_handle_t)); + + if(confbase) { + mesh->confbase = xstrdup(confbase); } - meshlink_handle_t *mesh = xzalloc(sizeof(meshlink_handle_t)); - mesh->confbase = xstrdup(confbase); mesh->appname = xstrdup(appname); mesh->devclass = devclass; mesh->discovery = true; @@ -1065,9 +1042,42 @@ meshlink_handle_t *meshlink_open_encrypted(const char *confbase, const char *nam } meshlink_handle_t *meshlink_open(const char *confbase, const char *name, const char *appname, dev_class_t devclass) { - return meshlink_open_encrypted(confbase, name, appname, devclass, NULL, 0); + if(!confbase || !*confbase) { + logger(NULL, MESHLINK_ERROR, "No confbase given!\n"); + meshlink_errno = MESHLINK_EINVAL; + return NULL; + } + + return meshlink_open_internal(confbase, name, appname, devclass, NULL, 0); +} + +meshlink_handle_t *meshlink_open_encrypted(const char *confbase, const char *name, const char *appname, dev_class_t devclass, const void *key, size_t keylen) { + if(!confbase || !*confbase) { + logger(NULL, MESHLINK_ERROR, "No confbase given!\n"); + meshlink_errno = MESHLINK_EINVAL; + return NULL; + } + + if(!key || !keylen) { + logger(NULL, MESHLINK_ERROR, "No key given!\n"); + meshlink_errno = MESHLINK_EINVAL; + return NULL; + } + + return meshlink_open_internal(confbase, name, appname, devclass, key, keylen); } +meshlink_handle_t *meshlink_open_ephemeral(const char *name, const char *appname, dev_class_t devclass) { + if(!name || !*name) { + logger(NULL, MESHLINK_ERROR, "No name given!\n"); + meshlink_errno = MESHLINK_EINVAL; + return NULL; + } + + return meshlink_open_internal(NULL, name, appname, devclass, NULL, 0); +} + + static void *meshlink_main_loop(void *arg) { meshlink_handle_t *mesh = arg; @@ -2021,38 +2031,63 @@ char *meshlink_export(meshlink_handle_t *mesh) { return NULL; } - config_t config; + // Create a config file on the fly. - // Get our config file + uint8_t buf[4096]; + packmsg_output_t out = {buf, sizeof(buf)}; + packmsg_add_uint32(&out, MESHLINK_CONFIG_VERSION); + packmsg_add_str(&out, mesh->name); pthread_mutex_lock(&(mesh->mesh_mutex)); - if(!config_read(mesh, mesh->self->name, &config)) { - meshlink_errno = MESHLINK_ESTORAGE; - pthread_mutex_unlock(&mesh->mesh_mutex); - return NULL; + 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 < 5; 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]); } pthread_mutex_unlock(&(mesh->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 - uint8_t *buf = xmalloc(((config.len + 4) * 4) / 3 + 4); - packmsg_output_t out = {buf, config.len + 4}; - packmsg_add_array(&out, 1); - packmsg_add_bin(&out, config.buf, config.len); - config_free(&config); + 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(&out)) { + if(!packmsg_output_ok(&out2)) { logger(mesh, MESHLINK_DEBUG, "Error creating export data\n"); meshlink_errno = MESHLINK_EINTERNAL; - free(buf); + free(buf2); return NULL; } - b64encode_urlsafe(buf, (char *)buf, packmsg_output_size(&out, buf)); + b64encode_urlsafe(buf2, (char *)buf2, packmsg_output_size(&out2, buf2)); - return (char *)buf; + return (char *)buf2; } bool meshlink_import(meshlink_handle_t *mesh, const char *data) { diff --git a/src/meshlink.h b/src/meshlink.h index b5062bea..e57763ff 100644 --- a/src/meshlink.h +++ b/src/meshlink.h @@ -189,6 +189,30 @@ extern meshlink_handle_t *meshlink_open(const char *confbase, const char *name, */ extern meshlink_handle_t *meshlink_open_encrypted(const char *confbase, const char *name, const char *appname, dev_class_t devclass, const void *key, size_t keylen); +/// Create an ephemeral MeshLink instance that does not store any state. +/** This function creates a MeshLink instance. + * No state is ever saved, so once this instance is closed, all its state is gone. + * + * The name given should be a unique identifier for this instance. + * + * This function returns a pointer to a struct meshlink_handle that will be allocated by MeshLink. + * When the application does no longer need to use this handle, it must call meshlink_close() to + * free its resources. + * + * This function does not start any network I/O yet. The application should + * first set callbacks, and then call meshlink_start(). + * + * @param name The name which this instance of the application will use in the mesh. + * After the function returns, the application is free to overwrite or free @a name @a. + * @param appname The application name which will be used in the mesh. + * After the function returns, the application is free to overwrite or free @a name @a. + * @param devclass The device class which will be used in the mesh. + * + * @return A pointer to a meshlink_handle_t which represents this instance of MeshLink, or NULL in case of an error. + * The pointer is valid until meshlink_close() is called. + */ +extern meshlink_handle_t *meshlink_open_ephemeral(const char *name, const char *appname, dev_class_t devclass); + /// Start MeshLink. /** This function causes MeshLink to open network sockets, make outgoing connections, and * create a new thread, which will handle all network I/O. diff --git a/src/net.h b/src/net.h index 19a53d21..74090ce8 100644 --- a/src/net.h +++ b/src/net.h @@ -109,6 +109,7 @@ extern int main_loop(struct meshlink_handle *mesh); extern void terminate_connection(struct meshlink_handle *mesh, struct connection_t *, bool); extern bool node_read_public_key(struct meshlink_handle *mesh, struct node_t *); extern bool node_read_full(struct meshlink_handle *mesh, struct node_t *); +extern bool node_read_from_config(struct meshlink_handle *mesh, struct node_t *, const config_t *config); extern bool read_ecdsa_public_key(struct meshlink_handle *mesh, struct connection_t *); extern bool read_ecdsa_private_key(struct meshlink_handle *mesh); extern bool node_write_config(struct meshlink_handle *mesh, struct node_t *); diff --git a/src/net_setup.c b/src/net_setup.c index 0cb79bf2..c7ad0b9d 100644 --- a/src/net_setup.c +++ b/src/net_setup.c @@ -111,6 +111,63 @@ bool node_read_public_key(meshlink_handle_t *mesh, node_t *n) { return true; } +/// Fill in node details from a config blob. +bool node_read_from_config(meshlink_handle_t *mesh, node_t *n, const config_t *config) { + if(n->canonical_address) { + return true; + } + + packmsg_input_t in = {config->buf, config->len}; + uint32_t version = packmsg_get_uint32(&in); + + if(version != MESHLINK_CONFIG_VERSION) { + return false; + } + + char *name = packmsg_get_str_dup(&in); + + if(!name) { + return false; + } + + if(n->name) { + if(strcmp(n->name, name)) { + free(name); + return false; + } + + free(name); + } else { + n->name = name; + } + + n->devclass = packmsg_get_int32(&in); + n->status.blacklisted = packmsg_get_bool(&in); + const void *key; + uint32_t len = packmsg_get_bin_raw(&in, &key); + + if(len != 32) { + return false; + } + + if(!ecdsa_active(n->ecdsa)) { + n->ecdsa = ecdsa_set_public_key(key); + } + + n->canonical_address = packmsg_get_str_dup(&in); + uint32_t count = packmsg_get_array(&in); + + if(count > 5) { + count = 5; + } + + for(uint32_t i = 0; i < count; i++) { + n->recent[i] = packmsg_get_sockaddr(&in); + } + + return packmsg_done(&in); +} + /// Read the full host config file. Used whenever we need to start an SPTPS session. bool node_read_full(meshlink_handle_t *mesh, node_t *n) { if(n->canonical_address) { @@ -196,37 +253,21 @@ bool node_write_config(meshlink_handle_t *mesh, node_t *n) { return config_write(mesh, n->name, &config); } -void load_all_nodes(meshlink_handle_t *mesh) { - DIR *dir; - struct dirent *ent; - char dname[PATH_MAX]; - - snprintf(dname, PATH_MAX, "%s" SLASH "hosts", mesh->confbase); - dir = opendir(dname); - - if(!dir) { - logger(mesh, MESHLINK_ERROR, "Could not open %s: %s", dname, strerror(errno)); +static void load_node(meshlink_handle_t *mesh, const char *name) { + if(!check_id(name)) { return; } - while((ent = readdir(dir))) { - if(!check_id(ent->d_name)) { - continue; - } - - node_t *n = lookup_node(mesh, ent->d_name); - - if(n) { - continue; - } + node_t *n = lookup_node(mesh, name); - n = new_node(); - n->name = xstrdup(ent->d_name); - node_read_devclass(mesh, n); - node_add(mesh, n); + if(n) { + return; } - closedir(dir); + n = new_node(); + n->name = xstrdup(name); + node_read_devclass(mesh, n); + node_add(mesh, n); } /* @@ -341,7 +382,7 @@ bool setup_myself(meshlink_handle_t *mesh) { graph(mesh); - load_all_nodes(mesh); + config_scan_all(mesh, load_node); /* Open sockets */ diff --git a/test/.gitignore b/test/.gitignore index cbb1c1ee..98cd87cd 100644 --- a/test/.gitignore +++ b/test/.gitignore @@ -7,6 +7,8 @@ /channels-fork /duplicate /echo-fork +/encrypted +/ephemeral /import-export /invite-join /sign-verify diff --git a/test/Makefile.am b/test/Makefile.am index e96213ed..45ad064c 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -6,6 +6,7 @@ TESTS = \ channels-cornercases.test \ duplicate.test \ encrypted.test \ + ephemeral.test \ import-export.test \ invite-join.test \ sign-verify.test \ @@ -29,6 +30,7 @@ check_PROGRAMS = \ duplicate \ echo-fork \ encrypted \ + ephemeral \ import-export \ invite-join \ sign-verify \ @@ -62,6 +64,9 @@ echo_fork_LDADD = ../src/libmeshlink.la encrypted_SOURCES = encrypted.c encrypted_LDADD = ../src/libmeshlink.la +ephemeral_SOURCES = ephemeral.c +ephemeral_LDADD = ../src/libmeshlink.la + import_export_SOURCES = import-export.c import_export_LDADD = ../src/libmeshlink.la diff --git a/test/ephemeral.c b/test/ephemeral.c new file mode 100644 index 00000000..10a6e2be --- /dev/null +++ b/test/ephemeral.c @@ -0,0 +1,74 @@ +#include +#include +#include + +#include "meshlink.h" + +void log_cb(meshlink_handle_t *mesh, meshlink_log_level_t level, const char *text) { + static struct timeval tv0; + struct timeval tv; + + if(tv0.tv_sec == 0) { + gettimeofday(&tv0, NULL); + } + + gettimeofday(&tv, NULL); + fprintf(stderr, "%u.%.03u ", (unsigned int)(tv.tv_sec - tv0.tv_sec), (unsigned int)tv.tv_usec / 1000); + + if(mesh) { + fprintf(stderr, "(%s) ", mesh->name); + } + + fprintf(stderr, "[%d] %s\n", level, text); +} + +int main() { + meshlink_set_log_cb(NULL, MESHLINK_DEBUG, log_cb); + + // Open two ephemeral meshlink instance. + + meshlink_handle_t *mesh1 = meshlink_open_ephemeral("foo", "ephemeral", DEV_CLASS_BACKBONE); + meshlink_handle_t *mesh2 = meshlink_open_ephemeral("bar", "ephemeral", DEV_CLASS_BACKBONE); + + assert(mesh1); + assert(mesh2); + + meshlink_set_log_cb(mesh1, MESHLINK_DEBUG, log_cb); + meshlink_set_log_cb(mesh2, MESHLINK_DEBUG, log_cb); + + // Exchange data + + assert(meshlink_import(mesh1, meshlink_export(mesh2))); + assert(meshlink_import(mesh2, meshlink_export(mesh1))); + + // Check that they know each other + + assert(meshlink_get_node(mesh1, "bar")); + assert(meshlink_get_node(mesh2, "foo")); + + // Close the ephemeral instances and reopen them. + + meshlink_close(mesh1); + meshlink_close(mesh2); + + mesh1 = meshlink_open_ephemeral("foo", "ephemeral", DEV_CLASS_BACKBONE); + mesh2 = meshlink_open_ephemeral("bar", "ephemeral", DEV_CLASS_BACKBONE); + + assert(mesh1); + assert(mesh2); + + meshlink_set_log_cb(mesh1, MESHLINK_DEBUG, log_cb); + meshlink_set_log_cb(mesh2, MESHLINK_DEBUG, log_cb); + + // Check that the nodes no longer know each other + + assert(!meshlink_get_node(mesh1, "bar")); + assert(!meshlink_get_node(mesh2, "foo")); + + // That's it. + + meshlink_close(mesh1); + meshlink_close(mesh2); + + return 0; +} diff --git a/test/ephemeral.test b/test/ephemeral.test new file mode 100755 index 00000000..9c49dbe8 --- /dev/null +++ b/test/ephemeral.test @@ -0,0 +1,3 @@ +#!/bin/sh + +./ephemeral