/* SPDX-License-Identifier: GPL-2.0 */ /** \file client_common.c Common functions of para_client and para_audiod. */ #include #include #include #include #include #include #include #include "client.lsg.h" #include "para.h" #include "error.h" #include "list.h" #include "lsu.h" #include "sched.h" #include "crypt.h" #include "net.h" #include "fd.h" #include "sideband.h" #include "string.h" #include "client.h" #include "buffer_tree.h" /** The size of the receiving buffer. */ #define CLIENT_BUFSIZE 4000 /** * Close the connection to para_server and free all resources. * * \param ct OK to pass NULL here, in which case the call is a no-op. * * \sa \ref client_open(). */ void client_close(struct client_task *ct) { if (!ct) return; free(ct->user); free(ct->key_file); lls_free_parse_result(ct->lpr, CLIENT_CMD_PTR); free(ct->challenge_hash); sb_free(ct->sbc[0]); sb_free(ct->sbc[1]); free(ct); } /* * This function asks the scheduler to monitor a file descriptor which * corresponds to an active connection. The descriptor is monitored for either * reading or writing, depending on the state of the connection. * * The context pointer is assumed to refer to a client task structure that was * initialized earlier by client_open(). */ static void client_pre_monitor(struct sched *s, void *context) { int ret; struct client_task *ct = context; if (ct->scc.fd < 0) return; switch (ct->status) { case CL_CONNECTED: case CL_SENT_AUTH: case CL_SENT_CH_RESPONSE: sched_monitor_readfd(ct->scc.fd, s); return; case CL_RECEIVED_WELCOME: case CL_RECEIVED_PROCEED: case CL_RECEIVED_CHALLENGE: sched_monitor_writefd(ct->scc.fd, s); return; case CL_SENDING: if (ct->btrn[1]) { ret = btr_node_status(ct->btrn[1], 0, BTR_NT_LEAF); if (ret < 0) sched_min_delay(s); else if (ret > 0) sched_monitor_writefd(ct->scc.fd, s); } __attribute__ ((fallthrough)); case CL_EXECUTING: if (ct->btrn[0]) { ret = btr_node_status(ct->btrn[0], 0, BTR_NT_ROOT); if (ret < 0) sched_min_delay(s); else if (ret > 0) sched_monitor_readfd(ct->scc.fd, s); } return; } } static int send_sb(struct client_task *ct, int channel, void *buf, size_t numbytes, enum sb_designator band, bool dont_free) { int ret, fd = ct->scc.fd; struct iovec iov[2]; if (!ct->sbc[channel]) { struct sb_buffer sbb; sb_transformation trafo = ct->status < CL_RECEIVED_PROCEED? NULL : sc_trafo; sbb = (typeof(sbb))SBB_INIT(band, buf, numbytes); ct->sbc[channel] = sb_new_send(&sbb, dont_free, trafo, ct->scc.send); } ret = sb_get_send_buffers(ct->sbc[channel], iov); ret = xwritev(fd, iov, ret); if (ret < 0) { sb_free(ct->sbc[channel]); ct->sbc[channel] = NULL; return ret; } if (sb_sent(ct->sbc[channel], ret)) { ct->sbc[channel] = NULL; return 1; } return 0; } static int recv_sb(struct client_task *ct, struct sb_buffer *result) { int ret; size_t n; sb_transformation trafo; void *trafo_context; struct iovec iov; if (ct->status < CL_SENT_CH_RESPONSE) trafo = trafo_context = NULL; else { trafo = sc_trafo; trafo_context = ct->scc.recv; } if (!ct->sbc[0]) ct->sbc[0] = sb_new_recv(0, trafo, trafo_context); again: sb_get_recv_buffer(ct->sbc[0], &iov); ret = read_nonblock(ct->scc.fd, iov.iov_base, iov.iov_len, &n); if (ret < 0) { sb_free(ct->sbc[0]); ct->sbc[0] = NULL; return ret; } if (n == 0) return 0; ret = sb_received(ct->sbc[0], n, result); if (ret < 0) return ret; if (ret == 0) goto again; ct->sbc[0] = NULL; return 1; } static char **parse_features(char *buf) { int i; const char id[] = "\nFeatures: "; char *p, *q, **features; p = strstr(buf, id); if (!p) return NULL; p += strlen(id); q = strchr(p, '\n'); if (!q) return NULL; *q = '\0'; create_argv(p, ",", &features); for (i = 0; features[i]; i++) PARA_INFO_LOG("server feature: %s\n", features[i]); return features; } static int dispatch_sbb(struct client_task *ct, struct sb_buffer *sbb) { int ret = 0; if (sideband_log("server", sbb)) goto deallocate; switch (sbb->band) { case SBD_AWAITING_DATA: ct->status = CL_SENDING; ret = 1; goto deallocate; case SBD_OUTPUT: btr_add_output(sbb->iov.iov_base, sbb->iov.iov_len, ct->btrn[0]); return 1; case SBD_EXIT__SUCCESS: ret = -E_SERVER_CMD_SUCCESS; goto deallocate; case SBD_EXIT__FAILURE: ret = -E_SERVER_CMD_FAILURE; goto deallocate; default: PARA_ERROR_LOG("invalid band %d\n", sbb->band); ret = -E_BAD_BAND; goto deallocate; } deallocate: free(sbb->iov.iov_base); sbb->iov.iov_base = NULL; return ret; } static int send_sb_command(struct client_task *ct) { int i; char *command, *p; size_t len = 0; unsigned num_inputs = lls_num_inputs(ct->lpr); if (ct->sbc[1]) return send_sb(ct, 0, NULL, 0, 0, false); for (i = 0; i < num_inputs; i++) len += strlen(lls_input(i, ct->lpr)) + 1; p = command = alloc(len); for (i = 0; i < num_inputs; i++) { const char *str = lls_input(i, ct->lpr); strcpy(p, str); p += strlen(str) + 1; } PARA_DEBUG_LOG("--> %s\n", command); return send_sb(ct, 0, command, len, SBD_COMMAND, false); } /* * This function reads or writes to the socket file descriptor which * corresponds to an established connection between the client and the server. * It depends on the current state of the connection and on the readiness of * the socket file descriptor which type of I/O is going to be performed. * Besides the initial handshake and authentication, the function sends the * server command and receives the output from the server, if any. * * The context pointer refers to a client task structure that was initialized * earlier by client_open(). */ static int client_post_monitor(struct sched *s, void *context) { struct client_task *ct = context; int ret = 0; size_t n; char buf[CLIENT_BUFSIZE]; ret = task_get_notification(ct->task); if (ret < 0) goto out; if (ct->scc.fd < 0) return 0; switch (ct->status) { case CL_CONNECTED: /* receive welcome message */ ret = read_nonblock(ct->scc.fd, buf, sizeof(buf), &n); if (ret < 0 || n == 0) goto out; ct->features = parse_features(buf); ct->status = CL_RECEIVED_WELCOME; return 0; case CL_RECEIVED_WELCOME: /* send auth command */ { /* * Request and use the sha256 feature unconditionally. After * 0.8.0 we no longer need to request it. */ if (!sched_write_ok(ct->scc.fd, s)) return 0; sprintf(buf, AUTH_REQUEST_MSG "%s", ct->user); PARA_INFO_LOG("--> %s\n", buf); ret = write_buffer(ct->scc.fd, buf); if (ret < 0) goto out; ct->status = CL_SENT_AUTH; return 0; } case CL_SENT_AUTH: /* * Receive challenge and session keys, decrypt the challenge and * send back the hash of the decrypted challenge. */ { /* decrypted challenge/session key buffer */ unsigned char *crypt_buf; struct sb_buffer sbb; ret = recv_sb(ct, &sbb); if (ret <= 0) goto out; if (sbb.band != SBD_CHALLENGE) { ret = -E_BAD_BAND; free(sbb.iov.iov_base); goto out; } n = sbb.iov.iov_len; PARA_INFO_LOG("<-- [challenge] (%zu bytes)\n", n); ret = apc_priv_decrypt(ct->key_file, &crypt_buf, sbb.iov.iov_base, n); free(sbb.iov.iov_base); if (ret < 0) goto out; if (ret != APC_CHALLENGE_SIZE + 2 * SESSION_KEY_LEN) { free(crypt_buf); ret = -E_DECRYPT; goto out; } ct->challenge_hash = alloc(HASH_SIZE); hash_function((char *)crypt_buf, APC_CHALLENGE_SIZE, ct->challenge_hash); hash_to_asc(ct->challenge_hash, buf); ct->scc.send = sc_new(crypt_buf + APC_CHALLENGE_SIZE, SESSION_KEY_LEN); ct->scc.recv = sc_new(crypt_buf + APC_CHALLENGE_SIZE + SESSION_KEY_LEN, SESSION_KEY_LEN); free(crypt_buf); PARA_INFO_LOG("--> %s\n", buf); ct->status = CL_RECEIVED_CHALLENGE; return 0; } case CL_RECEIVED_CHALLENGE: ret = send_sb(ct, 0, ct->challenge_hash, HASH_SIZE, SBD_CHALLENGE_RESPONSE, false); if (ret != 0) ct->challenge_hash = NULL; if (ret <= 0) goto out; ct->status = CL_SENT_CH_RESPONSE; goto out; case CL_SENT_CH_RESPONSE: /* read server response */ { struct sb_buffer sbb; ret = recv_sb(ct, &sbb); if (ret <= 0) goto out; free(sbb.iov.iov_base); if (sbb.band != SBD_PROCEED) ret = -E_BAD_BAND; else ct->status = CL_RECEIVED_PROCEED; goto out; } case CL_RECEIVED_PROCEED: /* concat args and send command */ { if (!sched_write_ok(ct->scc.fd, s)) return 0; ret = send_sb_command(ct); if (ret <= 0) goto out; ct->status = CL_EXECUTING; return 0; } case CL_SENDING: if (ct->btrn[1]) { char *buf2; size_t sz; ret = btr_node_status(ct->btrn[1], 0, BTR_NT_LEAF); if (ret == -E_EOF) { /* empty blob data packet indicates EOF */ PARA_INFO_LOG("blob sent\n"); ret = send_sb(ct, 1, NULL, 0, SBD_BLOB_DATA, true); if (ret >= 0) ret = -E_EOF; } if (ret < 0) goto close1; if (ret > 0 && sched_write_ok(ct->scc.fd, s)) { sz = btr_next_buffer(ct->btrn[1], &buf2); assert(sz); ret = send_sb(ct, 1, buf2, sz, SBD_BLOB_DATA, true); if (ret < 0) goto close1; if (ret > 0) btr_consume(ct->btrn[1], sz); } } __attribute__ ((fallthrough)); case CL_EXECUTING: if (ct->btrn[0]) { ret = btr_node_status(ct->btrn[0], 0, BTR_NT_ROOT); if (ret < 0) goto close0; if (ret > 0 && sched_read_ok(ct->scc.fd, s)) { struct sb_buffer sbb; ret = recv_sb(ct, &sbb); if (ret < 0) goto close0; if (ret > 0) { ret = dispatch_sbb(ct, &sbb); if (ret < 0) goto close0; } } } ret = 0; goto out; } close1: PARA_INFO_LOG("channel 1: %s\n", para_strerror(-ret)); btr_remove_node(&ct->btrn[1]); if (ct->btrn[0]) return 0; goto out; close0: PARA_INFO_LOG("channel 0: %s\n", para_strerror(-ret)); btr_remove_node(&ct->btrn[0]); if (ct->btrn[1] && ct->status == CL_SENDING) return 0; out: if (ret >= 0) return 0; btr_remove_node(&ct->btrn[0]); btr_remove_node(&ct->btrn[1]); PARA_NOTICE_LOG("closing connection (%s)\n", para_strerror(-ret)); if (ct->scc.fd >= 0) { close(ct->scc.fd); ct->scc.fd = -1; } free_argv(ct->features); ct->features = NULL; sc_free(ct->scc.recv); ct->scc.recv = NULL; sc_free(ct->scc.send); ct->scc.send = NULL; return ret; } /** * Connect to para_server and register the client task. * * \param ct The initialized client task structure. * \param s The scheduler instance to register the client task to. * \param parent The parent node of the client btr node. * \param child The child node of the client node. * * The client task structure must be allocated and initialized by \ref * client_parse_config() before this function is called. * * \return Standard. */ int client_connect(struct client_task *ct, struct sched *s, struct btr_node *parent, struct btr_node *child) { int ret; const char *host = CLIENT_OPT_STRING_VAL(HOSTNAME, ct->lpr); uint32_t port = CLIENT_OPT_UINT32_VAL(SERVER_PORT, ct->lpr); PARA_NOTICE_LOG("connecting %s:%u\n", host, port); ct->scc.fd = -1; ret = para_connect(IPPROTO_TCP, host, port); if (ret < 0) return ret; ct->scc.fd = ret; ret = mark_fd_nonblocking(ct->scc.fd); if (ret < 0) goto err_out; ct->status = CL_CONNECTED; ct->btrn[0] = btr_new_node(&(struct btr_node_description) EMBRACE(.name = "client recv", .parent = NULL, .child = child)); ct->btrn[1] = btr_new_node(&(struct btr_node_description) EMBRACE(.name = "client send", .parent = parent, .child = NULL)); ct->task = task_register(&(struct task_info) { .name = "client", .pre_monitor = client_pre_monitor, .post_monitor = client_post_monitor, .context = ct, }, s); return 1; err_out: close(ct->scc.fd); ct->scc.fd = -1; return ret; } static void handle_help_flag(struct lls_parse_result *lpr) { char *help; if (CLIENT_OPT_GIVEN(DETAILED_HELP, lpr)) help = lls_long_help(CLIENT_CMD_PTR); else if (CLIENT_OPT_GIVEN(HELP, lpr)) help = lls_short_help(CLIENT_CMD_PTR); else return; printf("%s\n", help); free(help); exit(EXIT_SUCCESS); } /** * Parse a client configuration and initialize a client task structure. * * \param argc Usual argument count. * \param argv Usual argument vector. * \param ct_ptr Filled in by this function. * * This checks the given command line options, sets default values for the * user name and the name of the rsa key file and reads further options from * the configuration file. * * On successful return, the client task argument will point to a dynamically * allocated and initialized client task structure. It should be freed later * by calling \ref client_close(). * * \return The number of non-option arguments on success, negative on errors. */ int client_parse_config(int argc, char *argv[], struct client_task **ct_ptr) { const struct lls_command *cmd = CLIENT_CMD_PTR; struct lls_parse_result *lpr; int ret; struct client_task *ct; char *kf = NULL, *user, *errctx, *home = para_homedir(); ret = lls(lls_parse(argc, argv, cmd, &lpr, &errctx)); if (ret < 0) goto out; version_handle_flag("client", CLIENT_OPT_GIVEN(VERSION, lpr)); handle_help_flag(lpr); ret = lsu_merge_config_file_options(CLIENT_OPT_STRING_VAL(CONFIG_FILE, lpr), "client.conf", &lpr, cmd, client_suite, 0U /* default flags */); if (ret < 0) goto out; /* success */ user = CLIENT_OPT_GIVEN(USER, lpr)? para_strdup(CLIENT_OPT_STRING_VAL(USER, lpr)) : para_logname(); if (CLIENT_OPT_GIVEN(KEY_FILE, lpr)) kf = para_strdup(CLIENT_OPT_STRING_VAL(KEY_FILE, lpr)); else { struct stat statbuf; kf = make_message("%s/.paraslash/key.%s", home, user); if (stat(kf, &statbuf) != 0) { /* assume file does not exist */ free(kf); kf = make_message("%s/.ssh/id_rsa", home); } } PARA_INFO_LOG("user: %s\n", user); PARA_INFO_LOG("key file: %s\n", kf); ct = zalloc(sizeof(*ct)); ct->scc.fd = -1; ct->lpr = lpr; ct->key_file = kf; ct->user = user; *ct_ptr = ct; ret = lls_num_inputs(lpr); out: free(home); if (ret < 0) { if (errctx) PARA_ERROR_LOG("%s\n", errctx); free(errctx); lls_free_parse_result(lpr, cmd); free(kf); *ct_ptr = NULL; } return ret; } /** * Parse the client configuration and open a connection to para_server. * * \param argc See \ref client_parse_config. * \param argv See \ref client_parse_config. * \param ct_ptr See \ref client_parse_config. * \param parent See \ref client_connect(). * \param child See \ref client_connect(). * \param sched See \ref client_connect(). * * This function combines \ref client_parse_config() and \ref * client_connect(). It is considered an error if no command was given, * i.e. if the number of non-option arguments is zero. * * \return Standard. */ int client_open(int argc, char *argv[], struct client_task **ct_ptr, struct btr_node *parent, struct btr_node *child, struct sched *sched) { int ret = client_parse_config(argc, argv, ct_ptr); if (ret < 0) return ret; if (ret == 0) { ret = -E_CLIENT_SYNTAX; goto fail; } ret = client_connect(*ct_ptr, sched, parent, child); if (ret < 0) goto fail; return 1; fail: client_close(*ct_ptr); *ct_ptr = NULL; return ret; }