| /* | |
| * libwebsockets - small server side websockets and web server implementation | |
| * | |
| * Copyright (C) 2010 - 2019 Andy Green <andy@warmcat.com> | |
| * | |
| * Permission is hereby granted, free of charge, to any person obtaining a copy | |
| * of this software and associated documentation files (the "Software"), to | |
| * deal in the Software without restriction, including without limitation the | |
| * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or | |
| * sell copies of the Software, and to permit persons to whom the Software is | |
| * furnished to do so, subject to the following conditions: | |
| * | |
| * The above copyright notice and this permission notice shall be included in | |
| * all copies or substantial portions of the Software. | |
| * | |
| * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS | |
| * IN THE SOFTWARE. | |
| */ | |
| #include "libwebsockets.h" | |
| #include "lws-ssh.h" | |
| #include <string.h> | |
| #include <stdlib.h> | |
| void *sshd_zalloc(size_t s) | |
| { | |
| void *p = malloc(s); | |
| if (p) | |
| memset(p, 0, s); | |
| return p; | |
| } | |
| uint32_t | |
| lws_g32(uint8_t **p) | |
| { | |
| uint32_t v = 0; | |
| v = (v << 8) | *((*p)++); | |
| v = (v << 8) | *((*p)++); | |
| v = (v << 8) | *((*p)++); | |
| v = (v << 8) | *((*p)++); | |
| return v; | |
| } | |
| uint32_t | |
| lws_p32(uint8_t *p, uint32_t v) | |
| { | |
| *p++ = v >> 24; | |
| *p++ = v >> 16; | |
| *p++ = v >> 8; | |
| *p++ = v; | |
| return v; | |
| } | |
| int | |
| lws_cstr(uint8_t **p, const char *s, uint32_t max) | |
| { | |
| uint32_t n = (uint32_t)strlen(s); | |
| if (n > max) | |
| return 1; | |
| lws_p32(*p, n); | |
| *p += 4; | |
| strcpy((char *)(*p), s); | |
| *p += n; | |
| return 0; | |
| } | |
| int | |
| lws_buf(uint8_t **p, void *s, uint32_t len) | |
| { | |
| lws_p32(*p, len); | |
| *p += 4; | |
| memcpy((char *)(*p), s, len); | |
| *p += len; | |
| return 0; | |
| } | |
| void | |
| write_task(struct per_session_data__sshd *pss, struct lws_ssh_channel *ch, | |
| int task) | |
| { | |
| pss->write_task[pss->wt_head] = task; | |
| pss->write_channel[pss->wt_head] = ch; | |
| pss->wt_head = (pss->wt_head + 1) & 7; | |
| lws_callback_on_writable(pss->wsi); | |
| } | |
| void | |
| write_task_insert(struct per_session_data__sshd *pss, struct lws_ssh_channel *ch, | |
| int task) | |
| { | |
| pss->wt_tail = (pss->wt_tail - 1) & 7; | |
| pss->write_task[pss->wt_tail] = task; | |
| pss->write_channel[pss->wt_tail] = ch; | |
| lws_callback_on_writable(pss->wsi); | |
| } | |
| void | |
| lws_pad_set_length(struct per_session_data__sshd *pss, void *start, uint8_t **p, | |
| struct lws_ssh_keys *keys) | |
| { | |
| uint32_t len = lws_ptr_diff(*p, start); | |
| uint8_t padc = 4, *bs = start; | |
| if (keys->full_length) | |
| len -= 4; | |
| if ((len + padc) & (keys->padding_alignment - 1)) | |
| padc += keys->padding_alignment - | |
| ((len + padc) & (keys->padding_alignment - 1)); | |
| bs[4] = padc; | |
| len += padc; | |
| if (!keys->valid) /* no crypto = pad with 00 */ | |
| while (padc--) | |
| *((*p)++) = 0; | |
| else { /* crypto active = pad with random */ | |
| lws_get_random(pss->vhd->context, *p, padc); | |
| (*p) += padc; | |
| } | |
| if (keys->full_length) | |
| len += 4; | |
| lws_p32(start, len - 4); | |
| } | |
| static uint32_t | |
| offer(struct per_session_data__sshd *pss, uint8_t *p, uint32_t len, int first, | |
| int *payload_len) | |
| { | |
| uint8_t *op = p, *lp, *end = p + len - 1; | |
| int n, padc = 4, keylen; | |
| char keyt[32]; | |
| uint8_t keybuf[256]; | |
| keylen = (int)get_gen_server_key_25519(pss, keybuf, (int)sizeof(keybuf)); | |
| if (!keylen) { | |
| lwsl_notice("get_gen_server_key failed\n"); | |
| return 1; | |
| } | |
| lwsl_info("keylen %d\n", keylen); | |
| n = ed25519_key_parse(keybuf, keylen, | |
| keyt, sizeof(keyt), NULL, NULL); | |
| if (n) { | |
| lwsl_notice("unable to parse server key: %d\n", n); | |
| return 1; | |
| } | |
| /* | |
| * byte SSH_MSG_KEXINIT | |
| * byte[16] cookie (random bytes) | |
| * name-list kex_algorithms | |
| * name-list server_host_key_algorithms | |
| * name-list encryption_algorithms_client_to_server | |
| * name-list encryption_algorithms_server_to_client | |
| * name-list mac_algorithms_client_to_server | |
| * name-list mac_algorithms_server_to_client | |
| * name-list compression_algorithms_client_to_server | |
| * name-list compression_algorithms_server_to_client | |
| * name-list langua->es_client_to_server | |
| * name-list langua->es_server_to_client | |
| * boolean first_kex_packet_follows | |
| * uint32 0 (reserved for future extension) | |
| */ | |
| p += 5; /* msg len + padding */ | |
| *p++ = SSH_MSG_KEXINIT; | |
| lws_get_random(pss->vhd->context, p, 16); | |
| p += 16; | |
| /* KEX algorithms */ | |
| lp = p; | |
| p += 4; | |
| n = lws_snprintf((char *)p, end - p, "curve25519-sha256@libssh.org"); | |
| p += lws_p32(lp, n); | |
| /* Server Host Key Algorithms */ | |
| lp = p; | |
| p += 4; | |
| n = lws_snprintf((char *)p, end - p, "%s", keyt); | |
| p += lws_p32(lp, n); | |
| /* Encryption Algorithms: C -> S */ | |
| lp = p; | |
| p += 4; | |
| // n = lws_snprintf((char *)p, end - p, "aes256-gcm@openssh.com"); | |
| n = lws_snprintf((char *)p, end - p, "chacha20-poly1305@openssh.com"); | |
| p += lws_p32(lp, n); | |
| /* Encryption Algorithms: S -> C */ | |
| lp = p; | |
| p += 4; | |
| // n = lws_snprintf((char *)p, end - p, "aes256-gcm@openssh.com"); | |
| n = lws_snprintf((char *)p, end - p, "chacha20-poly1305@openssh.com"); | |
| p += lws_p32(lp, n); | |
| /* MAC Algorithms: C -> S */ | |
| lp = p; | |
| p += 4; | |
| /* bogus: chacha20 does not use MACs, but 'none' is not offered */ | |
| n = lws_snprintf((char *)p, end - p, "hmac-sha2-256"); | |
| p += lws_p32(lp, n); | |
| /* MAC Algorithms: S -> C */ | |
| lp = p; | |
| p += 4; | |
| /* bogus: chacha20 does not use MACs, but 'none' is not offered */ | |
| n = lws_snprintf((char *)p, end - p, "hmac-sha2-256"); | |
| p += lws_p32(lp, n); | |
| /* Compression Algorithms: C -> S */ | |
| lp = p; | |
| p += 4; | |
| n = lws_snprintf((char *)p, end - p, "none"); | |
| p += lws_p32(lp, n); | |
| /* Compression Algorithms: S -> C */ | |
| lp = p; | |
| p += 4; | |
| n = lws_snprintf((char *)p, end - p, "none"); | |
| p += lws_p32(lp, n); | |
| if (p - op < 13 + padc + 8) | |
| return 0; | |
| /* Languages: C -> S */ | |
| *p++ = 0; | |
| *p++ = 0; | |
| *p++ = 0; | |
| *p++ = 0; | |
| /* Languages: S -> C */ | |
| *p++ = 0; | |
| *p++ = 0; | |
| *p++ = 0; | |
| *p++ = 0; | |
| /* First KEX packet coming */ | |
| *p++ = !!first; | |
| /* Reserved */ | |
| *p++ = 0; | |
| *p++ = 0; | |
| *p++ = 0; | |
| *p++ = 0; | |
| len = lws_ptr_diff(p, op); | |
| if (payload_len) | |
| /* starts at buf + 5 and excludes padding */ | |
| *payload_len = len - 5; | |
| /* we must give at least 4 bytes of 00 padding */ | |
| if ((len + padc) & 7) | |
| padc += 8 - ((len + padc) & 7); | |
| op[4] = padc; | |
| len += padc; | |
| while (padc--) | |
| *p++ = 0; | |
| /* recorded length does not include the uint32_t len itself */ | |
| lws_p32(op, len - 4); | |
| return len; | |
| } | |
| static int | |
| handle_name(struct per_session_data__sshd *pss) | |
| { | |
| struct lws_kex *kex = pss->kex; | |
| char keyt[32]; | |
| uint8_t keybuf[256]; | |
| int n = 0, len; | |
| switch (pss->parser_state) { | |
| case SSH_KEX_NL_KEX_ALGS: | |
| if (!strcmp(pss->name, "curve25519-sha256@libssh.org")) | |
| kex->match_bitfield |= 1; | |
| break; | |
| case SSH_KEX_NL_SHK_ALGS: | |
| len = (int)get_gen_server_key_25519(pss, keybuf, (int)sizeof(keybuf)); | |
| if (!len) | |
| break; | |
| if (ed25519_key_parse(keybuf, len, | |
| keyt, sizeof(keyt), | |
| NULL, NULL)) { | |
| lwsl_err("Unable to parse host key %d\n", n); | |
| } else { | |
| if (!strcmp(pss->name, keyt)) { | |
| kex->match_bitfield |= 2; | |
| break; | |
| } | |
| } | |
| break; | |
| case SSH_KEX_NL_EACTS_ALGS: | |
| if (!strcmp(pss->name, "chacha20-poly1305@openssh.com")) | |
| kex->match_bitfield |= 4; | |
| break; | |
| case SSH_KEX_NL_EASTC_ALGS: | |
| if (!strcmp(pss->name, "chacha20-poly1305@openssh.com")) | |
| kex->match_bitfield |= 8; | |
| break; | |
| case SSH_KEX_NL_MACTS_ALGS: | |
| if (!strcmp(pss->name, "hmac-sha2-256")) | |
| kex->match_bitfield |= 16; | |
| break; | |
| case SSH_KEX_NL_MASTC_ALGS: | |
| if (!strcmp(pss->name, "hmac-sha2-256")) | |
| kex->match_bitfield |= 32; | |
| break; | |
| case SSH_KEX_NL_CACTS_ALGS: | |
| if (!strcmp(pss->name, "none")) | |
| kex->match_bitfield |= 64; | |
| break; | |
| case SSH_KEX_NL_CASTC_ALGS: | |
| if (!strcmp(pss->name, "none")) | |
| kex->match_bitfield |= 128; | |
| break; | |
| case SSH_KEX_NL_LCTS_ALGS: | |
| case SSH_KEX_NL_LSTC_ALGS: | |
| break; | |
| default: | |
| break; | |
| } | |
| return 0; | |
| } | |
| static int | |
| lws_kex_create(struct per_session_data__sshd *pss) | |
| { | |
| pss->kex = sshd_zalloc(sizeof(struct lws_kex)); | |
| lwsl_info("%s\n", __func__); | |
| return !pss->kex; | |
| } | |
| static void | |
| lws_kex_destroy(struct per_session_data__sshd *pss) | |
| { | |
| if (!pss->kex) | |
| return; | |
| lwsl_info("Destroying KEX\n"); | |
| if (pss->kex->I_C) { | |
| free(pss->kex->I_C); | |
| pss->kex->I_C = NULL; | |
| } | |
| if (pss->kex->I_S) { | |
| free(pss->kex->I_S); | |
| pss->kex->I_S = NULL; | |
| } | |
| lws_explicit_bzero(pss->kex, sizeof(*pss->kex)); | |
| free(pss->kex); | |
| pss->kex = NULL; | |
| } | |
| static void | |
| ssh_free(void *p) | |
| { | |
| if (!p) | |
| return; | |
| lwsl_debug("%s: FREE %p\n", __func__, p); | |
| free(p); | |
| } | |
| #define ssh_free_set_NULL(x) if (x) { ssh_free(x); (x) = NULL; } | |
| static void | |
| lws_ua_destroy(struct per_session_data__sshd *pss) | |
| { | |
| if (!pss->ua) | |
| return; | |
| lwsl_info("%s\n", __func__); | |
| if (pss->ua->username) | |
| ssh_free(pss->ua->username); | |
| if (pss->ua->service) | |
| ssh_free(pss->ua->service); | |
| if (pss->ua->alg) | |
| ssh_free(pss->ua->alg); | |
| if (pss->ua->pubkey) | |
| ssh_free(pss->ua->pubkey); | |
| if (pss->ua->sig) { | |
| lws_explicit_bzero(pss->ua->sig, pss->ua->sig_len); | |
| ssh_free(pss->ua->sig); | |
| } | |
| lws_explicit_bzero(pss->ua, sizeof(*pss->ua)); | |
| free(pss->ua); | |
| pss->ua = NULL; | |
| } | |
| static int | |
| rsa_hash_alg_from_ident(const char *ident) | |
| { | |
| if (strcmp(ident, "ssh-rsa") == 0 || | |
| strcmp(ident, "ssh-rsa-cert-v01@openssh.com") == 0) | |
| return LWS_GENHASH_TYPE_SHA1; | |
| if (strcmp(ident, "rsa-sha2-256") == 0) | |
| return LWS_GENHASH_TYPE_SHA256; | |
| if (strcmp(ident, "rsa-sha2-512") == 0) | |
| return LWS_GENHASH_TYPE_SHA512; | |
| return -1; | |
| } | |
| static void | |
| state_get_string_alloc(struct per_session_data__sshd *pss, int next) | |
| { | |
| pss->parser_state = SSHS_GET_STRING_LEN_ALLOC; | |
| pss->state_after_string = next; | |
| } | |
| static void | |
| state_get_string(struct per_session_data__sshd *pss, int next) | |
| { | |
| pss->parser_state = SSHS_GET_STRING_LEN; | |
| pss->state_after_string = next; | |
| } | |
| static void | |
| state_get_u32(struct per_session_data__sshd *pss, int next) | |
| { | |
| pss->parser_state = SSHS_GET_U32; | |
| pss->state_after_string = next; | |
| } | |
| static struct lws_ssh_channel * | |
| ssh_get_server_ch(struct per_session_data__sshd *pss, uint32_t chi) | |
| { | |
| struct lws_ssh_channel *ch = pss->ch_list; | |
| while (ch) { | |
| if (ch->server_ch == chi) | |
| return ch; | |
| ch = ch->next; | |
| } | |
| return NULL; | |
| } | |
| #if 0 | |
| static struct lws_ssh_channel * | |
| ssh_get_peer_ch(struct per_session_data__sshd *pss, uint32_t chi) | |
| { | |
| struct lws_ssh_channel *ch = pss->ch_list; | |
| while (ch) { | |
| if (ch->sender_ch == chi) | |
| return ch; | |
| ch = ch->next; | |
| } | |
| return NULL; | |
| } | |
| #endif | |
| static void | |
| ssh_destroy_channel(struct per_session_data__sshd *pss, | |
| struct lws_ssh_channel *ch) | |
| { | |
| lws_start_foreach_llp(struct lws_ssh_channel **, ppch, pss->ch_list) { | |
| if (*ppch == ch) { | |
| lwsl_info("Deleting ch %p\n", ch); | |
| if (pss->vhd && pss->vhd->ops && | |
| pss->vhd->ops->channel_destroy) | |
| pss->vhd->ops->channel_destroy(ch->priv); | |
| *ppch = ch->next; | |
| if (ch->sub) | |
| free(ch->sub); | |
| free(ch); | |
| return; | |
| } | |
| } lws_end_foreach_llp(ppch, next); | |
| lwsl_notice("Failed to delete ch\n"); | |
| } | |
| static void | |
| lws_ssh_exec_finish(void *finish_handle, int retcode) | |
| { | |
| struct lws_ssh_channel *ch = (struct lws_ssh_channel *)finish_handle; | |
| struct per_session_data__sshd *pss = ch->pss; | |
| ch->retcode = retcode; | |
| write_task(pss, ch, SSH_WT_EXIT_STATUS); | |
| ch->scheduled_close = 1; | |
| write_task(pss, ch, SSH_WT_CH_CLOSE); | |
| } | |
| static int | |
| lws_ssh_parse_plaintext(struct per_session_data__sshd *pss, uint8_t *p, size_t len) | |
| { | |
| struct lws_gencrypto_keyelem e[LWS_GENCRYPTO_RSA_KEYEL_COUNT]; | |
| struct lws_genrsa_ctx ctx; | |
| struct lws_ssh_channel *ch; | |
| struct lws_subprotocol_scp *scp; | |
| uint8_t *pp, *ps, hash[64], *otmp; | |
| uint32_t m; | |
| int n; | |
| while (len --) { | |
| again: | |
| switch(pss->parser_state) { | |
| case SSH_INITIALIZE_TRANSIENT: | |
| pss->parser_state = SSHS_IDSTRING; | |
| pss->ctr = 0; | |
| pss->copy_to_I_C = 0; | |
| /* fallthru */ | |
| case SSHS_IDSTRING: | |
| if (*p == 0x0d) { | |
| pss->V_C[pss->npos] = '\0'; | |
| pss->npos = 0; | |
| lwsl_info("peer id: %s\n", pss->V_C); | |
| p++; | |
| pss->parser_state = SSHS_IDSTRING_CR; | |
| break; | |
| } | |
| if (pss->npos < sizeof(pss->V_C) - 1) | |
| pss->V_C[pss->npos++] = *p; | |
| p++; | |
| break; | |
| case SSHS_IDSTRING_CR: | |
| if (*p++ != 0x0a) { | |
| lwsl_notice("mangled id string\n"); | |
| return 1; | |
| } | |
| pss->ssh_sequence_ctr_cts = 0; | |
| pss->parser_state = SSHS_MSG_LEN; | |
| break; | |
| case SSHS_MSG_LEN: | |
| pss->msg_len = (pss->msg_len << 8) | *p++; | |
| if (++pss->ctr != 4) | |
| break; | |
| if (pss->active_keys_cts.valid) { | |
| uint8_t b[4]; | |
| POKE_U32(b, pss->msg_len); | |
| pss->msg_len = lws_chachapoly_get_length( | |
| &pss->active_keys_cts, | |
| pss->ssh_sequence_ctr_cts, b); | |
| } else | |
| pss->ssh_sequence_ctr_cts++; | |
| lwsl_info("msg len %d\n", pss->msg_len); | |
| pss->parser_state = SSHS_MSG_PADDING; | |
| pss->ctr = 0; | |
| pss->pos = 4; | |
| if (pss->msg_len < 2 + 4) { | |
| lwsl_notice("illegal msg size\n"); | |
| goto bail; | |
| } | |
| break; | |
| case SSHS_MSG_PADDING: | |
| pss->msg_padding = *p++; | |
| pss->parser_state = SSHS_MSG_ID; | |
| break; | |
| case SSHS_MSG_ID: | |
| pss->msg_id = *p++; | |
| pss->ctr = 0; | |
| switch (pss->msg_id) { | |
| case SSH_MSG_DISCONNECT: | |
| /* | |
| * byte SSH_MSG_DISCONNECT | |
| * uint32 reason code | |
| * string description in ISO-10646 | |
| * UTF-8 encoding [RFC3629] | |
| * string language tag [RFC3066] | |
| */ | |
| lwsl_notice("SSH_MSG_DISCONNECT\n"); | |
| state_get_u32(pss, SSHS_NVC_DISCONNECT_REASON); | |
| break; | |
| case SSH_MSG_IGNORE: | |
| lwsl_notice("SSH_MSG_IGNORE\n"); | |
| break; | |
| case SSH_MSG_UNIMPLEMENTED: | |
| lwsl_notice("SSH_MSG_UNIMPLEMENTED\n"); | |
| break; | |
| case SSH_MSG_DEBUG: | |
| lwsl_notice("SSH_MSG_DEBUG\n"); | |
| break; | |
| case SSH_MSG_SERVICE_REQUEST: | |
| lwsl_info("SSH_MSG_SERVICE_REQUEST\n"); | |
| /* payload is a string */ | |
| state_get_string(pss, SSHS_DO_SERVICE_REQUEST); | |
| break; | |
| case SSH_MSG_SERVICE_ACCEPT: | |
| lwsl_notice("SSH_MSG_ACCEPT\n"); | |
| break; | |
| case SSH_MSG_KEXINIT: | |
| if (pss->kex_state != | |
| KEX_STATE_EXPECTING_CLIENT_OFFER) { | |
| pss->parser_state = SSH_KEX_STATE_SKIP; | |
| break; | |
| } | |
| if (!pss->kex) { | |
| lwsl_notice("%s: SSH_MSG_KEXINIT: NULL pss->kex\n", __func__); | |
| goto bail; | |
| } | |
| pss->parser_state = SSH_KEX_STATE_COOKIE; | |
| pss->kex->I_C_payload_len = 0; | |
| pss->kex->I_C_alloc_len = pss->msg_len; | |
| pss->kex->I_C = sshd_zalloc(pss->kex->I_C_alloc_len); | |
| if (!pss->kex->I_C) { | |
| lwsl_notice("OOM 3\n"); | |
| goto bail; | |
| } | |
| pss->kex->I_C[pss->kex->I_C_payload_len++] = | |
| pss->msg_id; | |
| pss->copy_to_I_C = 1; | |
| break; | |
| case SSH_MSG_KEX_ECDH_INIT: | |
| pss->parser_state = SSH_KEX_STATE_ECDH_KEYLEN; | |
| break; | |
| case SSH_MSG_NEWKEYS: | |
| if (pss->kex_state != | |
| KEX_STATE_REPLIED_TO_OFFER && | |
| pss->kex_state != | |
| KEX_STATE_CRYPTO_INITIALIZED) { | |
| lwsl_notice("unexpected newkeys\n"); | |
| goto bail; | |
| } | |
| /* | |
| * it means we should now use the keys we | |
| * agreed on | |
| */ | |
| lwsl_info("Activating CTS keys\n"); | |
| pss->active_keys_cts = pss->kex->keys_next_cts; | |
| if (lws_chacha_activate(&pss->active_keys_cts)) | |
| goto bail; | |
| pss->kex->newkeys |= 2; | |
| if (pss->kex->newkeys == 3) | |
| lws_kex_destroy(pss); | |
| if (pss->msg_padding) { | |
| pss->copy_to_I_C = 0; | |
| pss->parser_state = | |
| SSHS_MSG_EAT_PADDING; | |
| break; | |
| } else { | |
| pss->parser_state = SSHS_MSG_LEN; | |
| } | |
| break; | |
| case SSH_MSG_USERAUTH_REQUEST: | |
| /* | |
| * byte SSH_MSG_USERAUTH_REQUEST | |
| * string user name in UTF-8 | |
| * encoding [RFC3629] | |
| * string service name in US-ASCII | |
| * string "publickey" | |
| * boolean FALSE | |
| * string public key alg | |
| * string public key blob | |
| */ | |
| lwsl_info("SSH_MSG_USERAUTH_REQUEST\n"); | |
| if (pss->ua) { | |
| lwsl_notice("pss->ua overwrite\n"); | |
| goto bail; | |
| } | |
| pss->ua = sshd_zalloc(sizeof(*pss->ua)); | |
| if (!pss->ua) | |
| goto bail; | |
| state_get_string_alloc(pss, SSHS_DO_UAR_SVC); | |
| /* username is destroyed with userauth struct */ | |
| if (!pss->sent_banner) { | |
| if (pss->vhd->ops->banner) | |
| write_task(pss, NULL, | |
| SSH_WT_UA_BANNER); | |
| pss->sent_banner = 1; | |
| } | |
| break; | |
| case SSH_MSG_USERAUTH_FAILURE: | |
| goto bail; | |
| case SSH_MSG_USERAUTH_SUCCESS: | |
| goto bail; | |
| case SSH_MSG_USERAUTH_BANNER: | |
| goto bail; | |
| case SSH_MSG_CHANNEL_OPEN: | |
| state_get_string(pss, SSHS_NVC_CHOPEN_TYPE); | |
| break; | |
| case SSH_MSG_CHANNEL_REQUEST: | |
| /* RFC4254 | |
| * | |
| * byte SSH_MSG_CHANNEL_REQUEST | |
| * uint32 recipient channel | |
| * string "pty-req" | |
| * boolean want_reply | |
| * string TERM environment variable value | |
| * (e.g., vt100) | |
| * uint32 terminal width, characters | |
| * (e.g., 80) | |
| * uint32 terminal height, rows (e.g., 24) | |
| * uint32 terminal width, px (e.g., 640) | |
| * uint32 terminal height, px (e.g., 480) | |
| * string encoded terminal modes | |
| */ | |
| state_get_u32(pss, SSHS_NVC_CHRQ_RECIP); | |
| break; | |
| case SSH_MSG_CHANNEL_EOF: | |
| /* RFC4254 | |
| * When a party will no longer send more data | |
| * to a channel, it SHOULD send | |
| * SSH_MSG_CHANNEL_EOF. | |
| * | |
| * byte SSH_MSG_CHANNEL_EOF | |
| * uint32 recipient channel | |
| */ | |
| state_get_u32(pss, SSHS_NVC_CH_EOF); | |
| break; | |
| case SSH_MSG_CHANNEL_CLOSE: | |
| /* RFC4254 | |
| * | |
| * byte SSH_MSG_CHANNEL_CLOSE | |
| * uint32 recipient channel | |
| * | |
| * This message does not consume window space | |
| * and can be sent even if no window space is | |
| * available. | |
| * | |
| * It is RECOMMENDED that all data sent before | |
| * this message be delivered to the actual | |
| * destination, if possible. | |
| */ | |
| state_get_u32(pss, SSHS_NVC_CH_CLOSE); | |
| break; | |
| case SSH_MSG_CHANNEL_DATA: | |
| /* RFC4254 | |
| * | |
| * byte SSH_MSG_CHANNEL_DATA | |
| * uint32 recipient channel | |
| * string data | |
| */ | |
| state_get_u32(pss, SSHS_NVC_CD_RECIP); | |
| break; | |
| case SSH_MSG_CHANNEL_WINDOW_ADJUST: | |
| /* RFC452 | |
| * | |
| * byte SSH_MSG_CHANNEL_WINDOW_ADJUST | |
| * uint32 recipient channel | |
| * uint32 bytes to add | |
| */ | |
| if (!pss->ch_list) | |
| goto bail; | |
| state_get_u32(pss, SSHS_NVC_WA_RECIP); | |
| break; | |
| default: | |
| lwsl_notice("unk msg_id %d\n", pss->msg_id); | |
| goto bail; | |
| } | |
| break; | |
| case SSH_KEX_STATE_COOKIE: | |
| if (pss->msg_len < 16 + 1 + 1 + (10 * 4) + 5) { | |
| lwsl_notice("sanity: kex length failed\n"); | |
| goto bail; | |
| } | |
| pss->kex->kex_cookie[pss->ctr++] = *p++; | |
| if (pss->ctr != sizeof(pss->kex->kex_cookie)) | |
| break; | |
| pss->parser_state = SSH_KEX_NL_KEX_ALGS_LEN; | |
| pss->ctr = 0; | |
| break; | |
| case SSH_KEX_NL_KEX_ALGS_LEN: | |
| case SSH_KEX_NL_SHK_ALGS_LEN: | |
| case SSH_KEX_NL_EACTS_ALGS_LEN: | |
| case SSH_KEX_NL_EASTC_ALGS_LEN: | |
| case SSH_KEX_NL_MACTS_ALGS_LEN: | |
| case SSH_KEX_NL_MASTC_ALGS_LEN: | |
| case SSH_KEX_NL_CACTS_ALGS_LEN: | |
| case SSH_KEX_NL_CASTC_ALGS_LEN: | |
| case SSH_KEX_NL_LCTS_ALGS_LEN: | |
| case SSH_KEX_NL_LSTC_ALGS_LEN: | |
| case SSH_KEX_STATE_ECDH_KEYLEN: | |
| pss->len = (pss->len << 8) | *p++; | |
| if (++pss->ctr != 4) | |
| break; | |
| switch (pss->parser_state) { | |
| case SSH_KEX_STATE_ECDH_KEYLEN: | |
| pss->parser_state = SSH_KEX_STATE_ECDH_Q_C; | |
| break; | |
| default: | |
| pss->parser_state++; | |
| if (pss->len == 0) | |
| pss->parser_state++; | |
| break; | |
| } | |
| pss->ctr = 0; | |
| pss->npos = 0; | |
| if (pss->msg_len - pss->pos < pss->len) { | |
| lwsl_notice("sanity: length %d - %d < %d\n", | |
| pss->msg_len, pss->pos, pss->len); | |
| goto bail; | |
| } | |
| break; | |
| case SSH_KEX_NL_KEX_ALGS: | |
| case SSH_KEX_NL_SHK_ALGS: | |
| case SSH_KEX_NL_EACTS_ALGS: | |
| case SSH_KEX_NL_EASTC_ALGS: | |
| case SSH_KEX_NL_MACTS_ALGS: | |
| case SSH_KEX_NL_MASTC_ALGS: | |
| case SSH_KEX_NL_CACTS_ALGS: | |
| case SSH_KEX_NL_CASTC_ALGS: | |
| case SSH_KEX_NL_LCTS_ALGS: | |
| case SSH_KEX_NL_LSTC_ALGS: | |
| if (*p != ',') { | |
| if (pss->npos < sizeof(pss->name) - 1) | |
| pss->name[pss->npos++] = *p; | |
| } else { | |
| pss->name[pss->npos] = '\0'; | |
| pss->npos = 0; | |
| handle_name(pss); | |
| } | |
| p++; | |
| if (!--pss->len) { | |
| pss->name[pss->npos] = '\0'; | |
| if (pss->npos) | |
| handle_name(pss); | |
| pss->parser_state++; | |
| break; | |
| } | |
| break; | |
| case SSH_KEX_FIRST_PKT: | |
| pss->first_coming = !!*p++; | |
| pss->parser_state = SSH_KEX_RESERVED; | |
| break; | |
| case SSH_KEX_RESERVED: | |
| pss->len = (pss->len << 8) | *p++; | |
| if (++pss->ctr != 4) | |
| break; | |
| pss->ctr = 0; | |
| if (pss->msg_padding) { | |
| pss->copy_to_I_C = 0; | |
| pss->parser_state = SSHS_MSG_EAT_PADDING; | |
| break; | |
| } | |
| pss->parser_state = SSHS_MSG_LEN; | |
| break; | |
| case SSH_KEX_STATE_ECDH_Q_C: | |
| if (pss->len != 32) { | |
| lwsl_notice("wrong key len\n"); | |
| goto bail; | |
| } | |
| pss->kex->Q_C[pss->ctr++] = *p++; | |
| if (pss->ctr != 32) | |
| break; | |
| lwsl_info("Q_C parsed\n"); | |
| pss->parser_state = SSHS_MSG_EAT_PADDING; | |
| break; | |
| case SSH_KEX_STATE_SKIP: | |
| if (pss->pos - 4 < pss->msg_len) { | |
| p++; | |
| break; | |
| } | |
| lwsl_debug("skip done pos %d, msg_len %d len=%ld, \n", | |
| pss->pos, pss->msg_len, (long)len); | |
| pss->parser_state = SSHS_MSG_LEN; | |
| pss->ctr = 0; | |
| break; | |
| case SSHS_MSG_EAT_PADDING: | |
| p++; | |
| if (--pss->msg_padding) | |
| break; | |
| if (pss->msg_len + 4 != pss->pos) { | |
| lwsl_notice("sanity: kex end mismatch %d %d\n", | |
| pss->pos, pss->msg_len); | |
| goto bail; | |
| } | |
| switch (pss->msg_id) { | |
| case SSH_MSG_KEX_ECDH_INIT: | |
| if (pss->kex->match_bitfield != 0xff) { | |
| lwsl_notice("unable to negotiate\n"); | |
| goto bail; | |
| } | |
| if (kex_ecdh(pss, pss->kex->kex_r, | |
| &pss->kex->kex_r_len)) { | |
| lwsl_notice("hex_ecdh failed\n"); | |
| goto bail; | |
| } | |
| write_task(pss, NULL, SSH_WT_OFFER_REPLY); | |
| break; | |
| } | |
| pss->parser_state = SSHS_MSG_LEN; | |
| pss->ctr = 0; | |
| break; | |
| case SSHS_GET_STRING_LEN: | |
| pss->npos = 0; | |
| pss->len = (pss->len << 8) | *p++; | |
| if (++pss->ctr != 4) | |
| break; | |
| pss->ctr = 0; | |
| pss->parser_state = SSHS_GET_STRING; | |
| break; | |
| case SSHS_GET_STRING: | |
| if (pss->npos >= sizeof(pss->name) - 1) { | |
| lwsl_notice("non-alloc string too big\n"); | |
| goto bail; | |
| } | |
| pss->name[pss->npos++] = *p++; | |
| if (pss->npos != pss->len) | |
| break; | |
| pss->name[pss->npos] = '\0'; | |
| pss->parser_state = pss->state_after_string; | |
| goto again; | |
| case SSHS_GET_STRING_LEN_ALLOC: | |
| pss->npos = 0; | |
| pss->len = (pss->len << 8) | *p++; | |
| if (++pss->ctr != 4) | |
| break; | |
| pss->ctr = 0; | |
| pss->last_alloc = sshd_zalloc(pss->len + 1); | |
| lwsl_debug("SSHS_GET_STRING_LEN_ALLOC: %p, state %d\n", | |
| pss->last_alloc, pss->state_after_string); | |
| if (!pss->last_alloc) { | |
| lwsl_notice("alloc string too big\n"); | |
| goto bail; | |
| } | |
| pss->parser_state = SSHS_GET_STRING_ALLOC; | |
| break; | |
| case SSHS_GET_STRING_ALLOC: | |
| if (pss->npos >= pss->len) | |
| goto bail; | |
| pss->last_alloc[pss->npos++] = *p++; | |
| if (pss->npos != pss->len) | |
| break; | |
| pss->last_alloc[pss->npos] = '\0'; | |
| pss->parser_state = pss->state_after_string; | |
| goto again; | |
| /* | |
| * User Authentication | |
| */ | |
| case SSHS_DO_SERVICE_REQUEST: | |
| pss->okayed_userauth = 1; | |
| pss->parser_state = SSHS_MSG_EAT_PADDING; | |
| /* | |
| * this only 'accepts' that we can negotiate auth for | |
| * this service, not accepts the auth | |
| */ | |
| write_task(pss, NULL, SSH_WT_UA_ACCEPT); | |
| break; | |
| case SSHS_DO_UAR_SVC: | |
| pss->ua->username = (char *)pss->last_alloc; | |
| pss->last_alloc = NULL; /* it was adopted */ | |
| state_get_string_alloc(pss, SSHS_DO_UAR_PUBLICKEY); | |
| /* destroyed with UA struct */ | |
| break; | |
| case SSHS_DO_UAR_PUBLICKEY: | |
| pss->ua->service = (char *)pss->last_alloc; | |
| pss->last_alloc = NULL; /* it was adopted */ | |
| /* Sect 5, RFC4252 | |
| * | |
| * The 'user name' and 'service name' are repeated in | |
| * every new authentication attempt, and MAY change. | |
| * | |
| * The server implementation MUST carefully check them | |
| * in every message, and MUST flush any accumulated | |
| * authentication states if they change. If it is | |
| * unable to flush an authentication state, it MUST | |
| * disconnect if the 'user name' or 'service name' | |
| * changes. | |
| */ | |
| if (pss->seen_auth_req_before && ( | |
| strcmp(pss->ua->username, | |
| pss->last_auth_req_username) || | |
| strcmp(pss->ua->service, | |
| pss->last_auth_req_service))) { | |
| lwsl_notice("username / svc changed\n"); | |
| goto bail; | |
| } | |
| pss->seen_auth_req_before = 1; | |
| lws_strncpy(pss->last_auth_req_username, | |
| pss->ua->username, | |
| sizeof(pss->last_auth_req_username)); | |
| lws_strncpy(pss->last_auth_req_service, | |
| pss->ua->service, | |
| sizeof(pss->last_auth_req_service)); | |
| if (strcmp(pss->ua->service, "ssh-connection")) | |
| goto ua_fail; | |
| state_get_string(pss, SSHS_NVC_DO_UAR_CHECK_PUBLICKEY); | |
| break; | |
| case SSHS_NVC_DO_UAR_CHECK_PUBLICKEY: | |
| if (!strcmp(pss->name, "none")) { | |
| /* we must fail it */ | |
| lwsl_info("got 'none' req, refusing\n"); | |
| goto ua_fail; | |
| } | |
| if (strcmp(pss->name, "publickey")) { | |
| lwsl_notice("expected 'publickey' got '%s'\n", | |
| pss->name); | |
| goto ua_fail; | |
| } | |
| pss->parser_state = SSHS_DO_UAR_SIG_PRESENT; | |
| break; | |
| case SSHS_DO_UAR_SIG_PRESENT: | |
| lwsl_info("SSHS_DO_UAR_SIG_PRESENT\n"); | |
| pss->ua->sig_present = *p++; | |
| state_get_string_alloc(pss, SSHS_NVC_DO_UAR_ALG); | |
| /* destroyed with UA struct */ | |
| break; | |
| case SSHS_NVC_DO_UAR_ALG: | |
| pss->ua->alg = (char *)pss->last_alloc; | |
| pss->last_alloc = NULL; /* it was adopted */ | |
| if (rsa_hash_alg_from_ident(pss->ua->alg) < 0) { | |
| lwsl_notice("unknown alg\n"); | |
| goto ua_fail; | |
| } | |
| state_get_string_alloc(pss, SSHS_NVC_DO_UAR_PUBKEY_BLOB); | |
| /* destroyed with UA struct */ | |
| break; | |
| case SSHS_NVC_DO_UAR_PUBKEY_BLOB: | |
| pss->ua->pubkey = pss->last_alloc; | |
| pss->last_alloc = NULL; /* it was adopted */ | |
| pss->ua->pubkey_len = pss->npos; | |
| /* | |
| * RFC4253 | |
| * | |
| * ssh-rsa | |
| * | |
| * The structure inside the blob is | |
| * | |
| * mpint e | |
| * mpint n | |
| * | |
| * Let's see if this key is authorized | |
| */ | |
| n = 1; | |
| if (pss->vhd->ops && pss->vhd->ops->is_pubkey_authorized) | |
| n = pss->vhd->ops->is_pubkey_authorized( | |
| pss->ua->username, pss->ua->alg, | |
| pss->ua->pubkey, pss->ua->pubkey_len); | |
| if (n) { | |
| lwsl_info("rejecting peer pubkey\n"); | |
| goto ua_fail; | |
| } | |
| if (pss->ua->sig_present) { | |
| state_get_string_alloc(pss, SSHS_NVC_DO_UAR_SIG); | |
| /* destroyed with UA struct */ | |
| break; | |
| } | |
| /* | |
| * This key is at least one we would be prepared | |
| * to accept if he really has it... since no sig | |
| * client should resend everything with a sig | |
| * appended. OK it and delete this initial UA | |
| */ | |
| write_task(pss, NULL, SSH_WT_UA_PK_OK); | |
| pss->parser_state = SSHS_MSG_EAT_PADDING; | |
| break; | |
| case SSHS_NVC_DO_UAR_SIG: | |
| /* | |
| * Now the pubkey is coming with a sig | |
| */ | |
| /* Sect 5.1 RFC4252 | |
| * | |
| * SSH_MSG_USERAUTH_SUCCESS MUST be sent only once. | |
| * When SSH_MSG_USERAUTH_SUCCESS has been sent, any | |
| * further authentication requests received after that | |
| * SHOULD be silently ignored. | |
| */ | |
| if (pss->ssh_auth_state == SSH_AUTH_STATE_GAVE_AUTH_IGNORE_REQS) { | |
| lwsl_info("Silently ignoring auth req after accepted\n"); | |
| goto ua_fail_silently; | |
| } | |
| lwsl_info("SSHS_DO_UAR_SIG\n"); | |
| pss->ua->sig = pss->last_alloc; | |
| pss->last_alloc = NULL; /* it was adopted */ | |
| pss->ua->sig_len = pss->npos; | |
| pss->parser_state = SSHS_MSG_EAT_PADDING; | |
| /* | |
| * RFC 4252 p9 | |
| * | |
| * The value of 'signature' is a signature with | |
| * the private host key of the following data, in | |
| * this order: | |
| * | |
| * string session identifier | |
| * byte SSH_MSG_USERAUTH_REQUEST | |
| * string user name | |
| * string service name | |
| * string "publickey" | |
| * boolean TRUE | |
| * string public key algorithm name | |
| * string public key to be used for auth | |
| * | |
| * We reproduce the signature plaintext and the | |
| * hash, and then decrypt the incoming signed block. | |
| * What comes out is some ASN1, in there is the | |
| * hash decrypted. We find it and confirm it | |
| * matches the hash we computed ourselves. | |
| * | |
| * First step is generate the sig plaintext | |
| */ | |
| n = 4 + 32 + | |
| 1 + | |
| 4 + (int)strlen(pss->ua->username) + | |
| 4 + (int)strlen(pss->ua->service) + | |
| 4 + 9 + | |
| 1 + | |
| 4 + (int)strlen(pss->ua->alg) + | |
| 4 + (int)pss->ua->pubkey_len; | |
| ps = sshd_zalloc(n); | |
| if (!ps) { | |
| lwsl_notice("OOM 4\n"); | |
| goto ua_fail; | |
| } | |
| pp = ps; | |
| lws_buf(&pp, pss->session_id, 32); | |
| *pp++ = SSH_MSG_USERAUTH_REQUEST; | |
| lws_cstr(&pp, pss->ua->username, 64); | |
| lws_cstr(&pp, pss->ua->service, 64); | |
| lws_cstr(&pp, "publickey", 64); | |
| *pp++ = 1; | |
| lws_cstr(&pp, pss->ua->alg, 64); | |
| lws_buf(&pp, pss->ua->pubkey, pss->ua->pubkey_len); | |
| /* Next hash the plaintext */ | |
| if (lws_genhash_init(&pss->ua->hash_ctx, | |
| rsa_hash_alg_from_ident(pss->ua->alg))) { | |
| lwsl_notice("genhash init failed\n"); | |
| free(ps); | |
| goto ua_fail; | |
| } | |
| if (lws_genhash_update(&pss->ua->hash_ctx, ps, pp - ps)) { | |
| lwsl_notice("genhash update failed\n"); | |
| free(ps); | |
| goto ua_fail; | |
| } | |
| lws_genhash_destroy(&pss->ua->hash_ctx, hash); | |
| free(ps); | |
| /* | |
| * Prepare the RSA decryption context: load in | |
| * the E and N factors | |
| */ | |
| memset(e, 0, sizeof(e)); | |
| pp = pss->ua->pubkey; | |
| m = lws_g32(&pp); | |
| pp += m; | |
| m = lws_g32(&pp); | |
| e[LWS_GENCRYPTO_RSA_KEYEL_E].buf = pp; | |
| e[LWS_GENCRYPTO_RSA_KEYEL_E].len = m; | |
| pp += m; | |
| m = lws_g32(&pp); | |
| e[LWS_GENCRYPTO_RSA_KEYEL_N].buf = pp; | |
| e[LWS_GENCRYPTO_RSA_KEYEL_N].len = m; | |
| if (lws_genrsa_create(&ctx, e, pss->vhd->context, | |
| LGRSAM_PKCS1_1_5, | |
| LWS_GENHASH_TYPE_UNKNOWN)) | |
| goto ua_fail; | |
| /* | |
| * point to the encrypted signature payload we | |
| * were sent | |
| */ | |
| pp = pss->ua->sig; | |
| m = lws_g32(&pp); | |
| pp += m; | |
| m = lws_g32(&pp); | |
| /* | |
| * decrypt it, resulting in an error, or some ASN1 | |
| * including the decrypted signature | |
| */ | |
| otmp = sshd_zalloc(m); | |
| if (!otmp) | |
| /* ua_fail1 frees bn_e, bn_n and rsa */ | |
| goto ua_fail1; | |
| n = lws_genrsa_public_decrypt(&ctx, pp, m, otmp, m); | |
| if (n > 0) { | |
| /* the decrypted sig is in ASN1 format */ | |
| m = 0; | |
| while ((int)m < n) { | |
| /* sig payload */ | |
| if (otmp[m] == 0x04 && | |
| otmp[m + 1] == lws_genhash_size( | |
| pss->ua->hash_ctx.type)) { | |
| m = memcmp(&otmp[m + 2], hash, | |
| lws_genhash_size(pss->ua->hash_ctx.type)); | |
| break; | |
| } | |
| /* go into these */ | |
| if (otmp[m] == 0x30) { | |
| m += 2; | |
| continue; | |
| } | |
| /* otherwise skip payloads */ | |
| m += otmp[m + 1] + 2; | |
| } | |
| } | |
| free(otmp); | |
| lws_genrsa_destroy(&ctx); | |
| /* | |
| * if no good, m is nonzero and inform peer | |
| */ | |
| if (n <= 0) { | |
| lwsl_notice("hash sig verify fail: %d\n", m); | |
| goto ua_fail; | |
| } | |
| /* if it checks out, inform peer */ | |
| lwsl_info("sig check OK\n"); | |
| /* Sect 5.1 RFC4252 | |
| * | |
| * SSH_MSG_USERAUTH_SUCCESS MUST be sent only once. | |
| * When SSH_MSG_USERAUTH_SUCCESS has been sent, any | |
| * further authentication requests received after that | |
| * SHOULD be silently ignored. | |
| */ | |
| pss->ssh_auth_state = SSH_AUTH_STATE_GAVE_AUTH_IGNORE_REQS; | |
| write_task(pss, NULL, SSH_WT_UA_SUCCESS); | |
| lws_ua_destroy(pss); | |
| break; | |
| /* | |
| * Channels | |
| */ | |
| case SSHS_GET_U32: | |
| pss->len = (pss->len << 8) | *p++; | |
| if (++pss->ctr != 4) | |
| break; | |
| pss->ctr = 0; | |
| pss->parser_state = pss->state_after_string; | |
| goto again; | |
| /* | |
| * Channel: Disconnect | |
| */ | |
| case SSHS_NVC_DISCONNECT_REASON: | |
| pss->disconnect_reason = pss->len; | |
| state_get_string_alloc(pss, SSHS_NVC_DISCONNECT_DESC); | |
| break; | |
| case SSHS_NVC_DISCONNECT_DESC: | |
| pss->disconnect_desc = (char *)pss->last_alloc; | |
| pss->last_alloc = NULL; /* it was adopted */ | |
| state_get_string(pss, SSHS_NVC_DISCONNECT_LANG); | |
| break; | |
| case SSHS_NVC_DISCONNECT_LANG: | |
| lwsl_notice("SSHS_NVC_DISCONNECT_LANG\n"); | |
| if (pss->vhd->ops && pss->vhd->ops->disconnect_reason) | |
| pss->vhd->ops->disconnect_reason( | |
| pss->disconnect_reason, | |
| pss->disconnect_desc, pss->name); | |
| ssh_free_set_NULL(pss->last_alloc); | |
| break; | |
| /* | |
| * Channel: Open | |
| */ | |
| case SSHS_NVC_CHOPEN_TYPE: | |
| /* channel open */ | |
| if (strcmp(pss->name, "session")) { | |
| lwsl_notice("Failing on not session\n"); | |
| pss->reason = 3; | |
| goto ch_fail; | |
| } | |
| lwsl_info("SSHS_NVC_CHOPEN_TYPE: creating session\n"); | |
| pss->ch_temp = sshd_zalloc(sizeof(*pss->ch_temp)); | |
| if (!pss->ch_temp) | |
| return -1; | |
| pss->ch_temp->type = SSH_CH_TYPE_SESSION; | |
| pss->ch_temp->pss = pss; | |
| state_get_u32(pss, SSHS_NVC_CHOPEN_SENDER_CH); | |
| break; | |
| case SSHS_NVC_CHOPEN_SENDER_CH: | |
| pss->ch_temp->sender_ch = pss->len; | |
| state_get_u32(pss, SSHS_NVC_CHOPEN_WINSIZE); | |
| break; | |
| case SSHS_NVC_CHOPEN_WINSIZE: | |
| lwsl_info("Initial window set to %d\n", pss->len); | |
| pss->ch_temp->window = pss->len; | |
| state_get_u32(pss, SSHS_NVC_CHOPEN_PKTSIZE); | |
| break; | |
| case SSHS_NVC_CHOPEN_PKTSIZE: | |
| pss->ch_temp->max_pkt = pss->len; | |
| pss->ch_temp->peer_window_est = LWS_SSH_INITIAL_WINDOW; | |
| pss->ch_temp->server_ch = pss->next_ch_num++; | |
| /* | |
| * add us to channel list... leave as ch_temp | |
| * as write task needs it and will NULL down | |
| */ | |
| lwsl_info("creating new session ch\n"); | |
| pss->ch_temp->next = pss->ch_list; | |
| pss->ch_list = pss->ch_temp; | |
| if (pss->vhd->ops && pss->vhd->ops->channel_create) | |
| pss->vhd->ops->channel_create(pss->wsi, | |
| &pss->ch_temp->priv); | |
| write_task(pss, pss->ch_temp, SSH_WT_CH_OPEN_CONF); | |
| pss->parser_state = SSHS_MSG_EAT_PADDING; | |
| break; | |
| /* | |
| * SSH_MSG_CHANNEL_REQUEST | |
| */ | |
| case SSHS_NVC_CHRQ_RECIP: | |
| pss->ch_recip = pss->len; | |
| state_get_string(pss, SSHS_NVC_CHRQ_TYPE); | |
| break; | |
| case SSHS_NVC_CHRQ_TYPE: | |
| pss->parser_state = SSHS_CHRQ_WANT_REPLY; | |
| break; | |
| case SSHS_CHRQ_WANT_REPLY: | |
| pss->rq_want_reply = *p++; | |
| lwsl_info("SSHS_CHRQ_WANT_REPLY: %s, wantrep: %d\n", | |
| pss->name, pss->rq_want_reply); | |
| pss->ch_temp = ssh_get_server_ch(pss, pss->ch_recip); | |
| /* after this they differ by the request */ | |
| /* | |
| * a PTY for a shell | |
| */ | |
| if (!strcmp(pss->name, "pty-req")) { | |
| state_get_string(pss, SSHS_NVC_CHRQ_TERM); | |
| break; | |
| } | |
| /* | |
| * a shell | |
| */ | |
| if (!strcmp(pss->name, "shell")) { | |
| pss->channel_doing_spawn = pss->ch_temp->server_ch; | |
| if (pss->vhd->ops && pss->vhd->ops->shell && | |
| !pss->vhd->ops->shell(pss->ch_temp->priv, | |
| pss->wsi, | |
| lws_ssh_exec_finish, pss->ch_temp)) { | |
| if (pss->rq_want_reply) | |
| write_task_insert(pss, pss->ch_temp, | |
| SSH_WT_CHRQ_SUCC); | |
| pss->parser_state = SSHS_MSG_EAT_PADDING; | |
| break; | |
| } | |
| goto chrq_fail; | |
| } | |
| /* | |
| * env vars to be set in the shell | |
| */ | |
| if (!strcmp(pss->name, "env")) { | |
| state_get_string(pss, SSHS_NVC_CHRQ_ENV_NAME); | |
| break; | |
| } | |
| /* | |
| * exec something | |
| */ | |
| if (!strcmp(pss->name, "exec")) { | |
| state_get_string_alloc(pss, SSHS_NVC_CHRQ_EXEC_CMD); | |
| break; | |
| } | |
| /* | |
| * spawn a subsystem | |
| */ | |
| if (!strcmp(pss->name, "subsystem")) { | |
| lwsl_notice("subsystem\n"); | |
| state_get_string_alloc(pss, | |
| SSHS_NVC_CHRQ_SUBSYSTEM); | |
| break; | |
| } | |
| if (pss->rq_want_reply) | |
| goto chrq_fail; | |
| pss->parser_state = SSH_KEX_STATE_SKIP; | |
| break; | |
| /* CHRQ pty-req */ | |
| case SSHS_NVC_CHRQ_TERM: | |
| memcpy(pss->args.pty.term, pss->name, | |
| sizeof(pss->args.pty.term) - 1); | |
| state_get_u32(pss, SSHS_NVC_CHRQ_TW); | |
| break; | |
| case SSHS_NVC_CHRQ_TW: | |
| pss->args.pty.width_ch = pss->len; | |
| state_get_u32(pss, SSHS_NVC_CHRQ_TH); | |
| break; | |
| case SSHS_NVC_CHRQ_TH: | |
| pss->args.pty.height_ch = pss->len; | |
| state_get_u32(pss, SSHS_NVC_CHRQ_TWP); | |
| break; | |
| case SSHS_NVC_CHRQ_TWP: | |
| pss->args.pty.width_px = pss->len; | |
| state_get_u32(pss, SSHS_NVC_CHRQ_THP); | |
| break; | |
| case SSHS_NVC_CHRQ_THP: | |
| pss->args.pty.height_px = pss->len; | |
| state_get_string_alloc(pss, SSHS_NVC_CHRQ_MODES); | |
| break; | |
| case SSHS_NVC_CHRQ_MODES: | |
| /* modes is a stream of byte-pairs, not a string */ | |
| pss->args.pty.modes = (char *)pss->last_alloc; | |
| pss->last_alloc = NULL; /* it was adopted */ | |
| pss->args.pty.modes_len = pss->npos; | |
| n = 0; | |
| if (pss->vhd->ops && pss->vhd->ops->pty_req) | |
| n = pss->vhd->ops->pty_req(pss->ch_temp->priv, | |
| &pss->args.pty); | |
| ssh_free_set_NULL(pss->args.pty.modes); | |
| if (n) | |
| goto chrq_fail; | |
| if (pss->rq_want_reply) | |
| write_task(pss, pss->ch_temp, SSH_WT_CHRQ_SUCC); | |
| pss->parser_state = SSHS_MSG_EAT_PADDING; | |
| break; | |
| /* CHRQ env */ | |
| case SSHS_NVC_CHRQ_ENV_NAME: | |
| strcpy(pss->args.aux, pss->name); | |
| state_get_string(pss, SSHS_NVC_CHRQ_ENV_VALUE); | |
| break; | |
| case SSHS_NVC_CHRQ_ENV_VALUE: | |
| if (pss->vhd->ops && pss->vhd->ops->set_env) | |
| if (pss->vhd->ops->set_env(pss->ch_temp->priv, | |
| pss->args.aux, pss->name)) | |
| goto chrq_fail; | |
| pss->parser_state = SSHS_MSG_EAT_PADDING; | |
| break; | |
| /* CHRQ exec */ | |
| case SSHS_NVC_CHRQ_EXEC_CMD: | |
| /* | |
| * byte SSH_MSG_CHANNEL_REQUEST | |
| * uint32 recipient channel | |
| * string "exec" | |
| * boolean want reply | |
| * string command | |
| * | |
| * This message will request that the server start the | |
| * execution of the given command. The 'command' string | |
| * may contain a path. Normal precautions MUST be taken | |
| * to prevent the execution of unauthorized commands. | |
| * | |
| * scp sends "scp -t /path/..." | |
| */ | |
| lwsl_info("exec cmd: %s %02X\n", pss->last_alloc, *p); | |
| pss->channel_doing_spawn = pss->ch_temp->server_ch; | |
| if (pss->vhd->ops && pss->vhd->ops->exec && | |
| !pss->vhd->ops->exec(pss->ch_temp->priv, pss->wsi, | |
| (const char *)pss->last_alloc, | |
| lws_ssh_exec_finish, pss->ch_temp)) { | |
| ssh_free_set_NULL(pss->last_alloc); | |
| if (pss->rq_want_reply) | |
| write_task_insert(pss, pss->ch_temp, | |
| SSH_WT_CHRQ_SUCC); | |
| pss->parser_state = SSHS_MSG_EAT_PADDING; | |
| break; | |
| } | |
| /* | |
| * even if he doesn't want to exec it, we know how to | |
| * fake scp | |
| */ | |
| /* we only alloc "exec" of scp for scp destination */ | |
| n = 1; | |
| if (pss->last_alloc[0] != 's' || | |
| pss->last_alloc[1] != 'c' || | |
| pss->last_alloc[2] != 'p' || | |
| pss->last_alloc[3] != ' ') | |
| /* disallow it */ | |
| n = 0; | |
| ssh_free_set_NULL(pss->last_alloc); | |
| if (!n) | |
| goto chrq_fail; | |
| /* our channel speaks SCP protocol now */ | |
| scp = sshd_zalloc(sizeof(*scp)); | |
| if (!scp) | |
| return -1; | |
| pss->ch_temp->type = SSH_CH_TYPE_SCP; | |
| pss->ch_temp->sub = (lws_subprotocol *)scp; | |
| scp->ips = SSHS_SCP_COLLECTSTR; | |
| if (pss->rq_want_reply) | |
| write_task(pss, pss->ch_temp, SSH_WT_CHRQ_SUCC); | |
| /* we start the scp protocol first by sending an ACK */ | |
| write_task(pss, pss->ch_temp, SSH_WT_SCP_ACK_OKAY); | |
| pss->parser_state = SSHS_MSG_EAT_PADDING; | |
| break; | |
| case SSHS_NVC_CHRQ_SUBSYSTEM: | |
| lwsl_notice("subsystem: %s", pss->last_alloc); | |
| n = 0; | |
| #if 0 | |
| if (!strcmp(pss->name, "sftp")) { | |
| lwsl_notice("SFTP session\n"); | |
| pss->ch_temp->type = SSH_CH_TYPE_SFTP; | |
| n = 1; | |
| } | |
| #endif | |
| ssh_free_set_NULL(pss->last_alloc); | |
| // if (!n) | |
| goto ch_fail; | |
| #if 0 | |
| if (pss->rq_want_reply) | |
| write_task(pss, ssh_get_server_ch(pss, | |
| pss->ch_recip), SSH_WT_CHRQ_SUCC); | |
| pss->parser_state = SSHS_MSG_EAT_PADDING; | |
| break; | |
| #endif | |
| /* SSH_MSG_CHANNEL_DATA */ | |
| case SSHS_NVC_CD_RECIP: | |
| pss->ch_recip = pss->len; | |
| ch = ssh_get_server_ch(pss, pss->ch_recip); | |
| ch->peer_window_est -= pss->msg_len; | |
| if (pss->msg_len < sizeof(pss->name)) | |
| state_get_string(pss, SSHS_NVC_CD_DATA); | |
| else | |
| state_get_string_alloc(pss, | |
| SSHS_NVC_CD_DATA_ALLOC); | |
| break; | |
| case SSHS_NVC_CD_DATA_ALLOC: | |
| case SSHS_NVC_CD_DATA: | |
| /* | |
| * Actual protocol incoming payload | |
| */ | |
| if (pss->parser_state == SSHS_NVC_CD_DATA_ALLOC) | |
| pp = pss->last_alloc; | |
| else | |
| pp = (uint8_t *)pss->name; | |
| lwsl_info("SSHS_NVC_CD_DATA\n"); | |
| ch = ssh_get_server_ch(pss, pss->ch_recip); | |
| switch (ch->type) { | |
| case SSH_CH_TYPE_SCP: | |
| scp = &ch->sub->scp; | |
| switch (scp->ips) { | |
| case SSHS_SCP_COLLECTSTR: | |
| /* gather the ascii-coded headers */ | |
| for (n = 0; n < (int)pss->npos; n++) | |
| lwsl_notice("0x%02X %c\n", | |
| pp[n], pp[n]); | |
| /* Header triggers the transfer? */ | |
| if (pp[0] == 'C' && pp[pss->npos - 1] == '\x0a') { | |
| while (*pp != ' ' && *pp != '\x0a') | |
| pp++; | |
| if (*pp++ != ' ') { | |
| write_task(pss, ch, | |
| SSH_WT_SCP_ACK_ERROR); | |
| pss->parser_state = SSHS_MSG_EAT_PADDING; | |
| break; | |
| } | |
| scp->len = atoll((const char *)pp); | |
| lwsl_notice("scp payload %llu expected\n", | |
| (unsigned long long)scp->len); | |
| scp->ips = SSHS_SCP_PAYLOADIN; | |
| } | |
| /* ack it */ | |
| write_task(pss, pss->ch_temp, | |
| SSH_WT_SCP_ACK_OKAY); | |
| break; | |
| case SSHS_SCP_PAYLOADIN: | |
| /* the scp file payload */ | |
| if (pss->vhd->ops) | |
| pss->vhd->ops->rx(ch->priv, | |
| pss->wsi, pp, pss->npos); | |
| if (scp->len >= pss->npos) | |
| scp->len -= pss->npos; | |
| else | |
| scp->len = 0; | |
| if (!scp->len) { | |
| lwsl_notice("scp txfer completed\n"); | |
| scp->ips = SSHS_SCP_COLLECTSTR; | |
| break; | |
| } | |
| break; | |
| } | |
| break; | |
| default: /* scp payload */ | |
| if (pss->vhd->ops) | |
| pss->vhd->ops->rx(ch->priv, pss->wsi, | |
| pp, pss->npos); | |
| break; | |
| } | |
| if (pss->parser_state == SSHS_NVC_CD_DATA_ALLOC) | |
| ssh_free_set_NULL(pss->last_alloc); | |
| if (ch->peer_window_est < 32768) { | |
| write_task(pss, ch, SSH_WT_WINDOW_ADJUST); | |
| ch->peer_window_est += 32768; | |
| lwsl_info("extra peer WINDOW_ADJUST (~ %d)\n", | |
| ch->peer_window_est); | |
| } | |
| pss->parser_state = SSHS_MSG_EAT_PADDING; | |
| break; | |
| case SSHS_NVC_WA_RECIP: | |
| pss->ch_recip = pss->len; | |
| state_get_u32(pss, SSHS_NVC_WA_ADD); | |
| break; | |
| case SSHS_NVC_WA_ADD: | |
| ch = ssh_get_server_ch(pss, pss->ch_recip); | |
| if (ch) { | |
| ch->window += pss->len; | |
| lwsl_notice("got additional window %d (now %d)\n", | |
| pss->len, ch->window); | |
| } | |
| pss->parser_state = SSHS_MSG_EAT_PADDING; | |
| break; | |
| /* | |
| * channel close | |
| */ | |
| case SSHS_NVC_CH_EOF: | |
| /* | |
| * No explicit response is sent to this | |
| * message. However, the application may send | |
| * EOF to whatever is at the other end of the | |
| * channel. Note that the channel remains open | |
| * after this message, and more data may still | |
| * be sent in the other direction. This message | |
| * does not consume window space and can be sent | |
| * even if no window space is available. | |
| */ | |
| lwsl_notice("SSH_MSG_CHANNEL_EOF: %d\n", pss->ch_recip); | |
| ch = ssh_get_server_ch(pss, pss->ch_recip); | |
| if (!ch) { | |
| lwsl_notice("unknown ch %d\n", pss->ch_recip); | |
| return -1; | |
| } | |
| if (!ch->scheduled_close) { | |
| lwsl_notice("scheduling CLOSE\n"); | |
| ch->scheduled_close = 1; | |
| write_task(pss, ch, SSH_WT_CH_CLOSE); | |
| } | |
| pss->parser_state = SSHS_MSG_EAT_PADDING; | |
| break; | |
| case SSHS_NVC_CH_CLOSE: | |
| /* | |
| * When either party wishes to terminate the | |
| * channel, it sends SSH_MSG_CHANNEL_CLOSE. | |
| * Upon receiving this message, a party MUST | |
| * send back an SSH_MSG_CHANNEL_CLOSE unless it | |
| * has already sent this message for the | |
| * channel. The channel is considered closed | |
| * for a party when it has both sent and | |
| * received SSH_MSG_CHANNEL_CLOSE, and the | |
| * party may then reuse the channel number. | |
| * A party MAY send SSH_MSG_CHANNEL_CLOSE | |
| * without having sent or received | |
| * SSH_MSG_CHANNEL_EOF. | |
| */ | |
| lwsl_notice("SSH_MSG_CHANNEL_CLOSE ch %d\n", | |
| pss->ch_recip); | |
| ch = ssh_get_server_ch(pss, pss->ch_recip); | |
| if (!ch) | |
| goto bail; | |
| pss->parser_state = SSHS_MSG_EAT_PADDING; | |
| if (ch->sent_close) { | |
| /* | |
| * This is acking our sent close... | |
| * we can destroy the channel with no | |
| * further communication. | |
| */ | |
| ssh_destroy_channel(pss, ch); | |
| break; | |
| } | |
| ch->received_close = 1; | |
| ch->scheduled_close = 1; | |
| write_task(pss, ch, SSH_WT_CH_CLOSE); | |
| break; | |
| default: | |
| break; | |
| chrq_fail: | |
| lwsl_notice("chrq_fail\n"); | |
| write_task(pss, pss->ch_temp, SSH_WT_CHRQ_FAILURE); | |
| pss->parser_state = SSH_KEX_STATE_SKIP; | |
| break; | |
| ch_fail: | |
| if (pss->ch_temp) { | |
| free(pss->ch_temp); | |
| pss->ch_temp = NULL; | |
| } | |
| write_task(pss, pss->ch_temp, SSH_WT_CH_FAILURE); | |
| pss->parser_state = SSH_KEX_STATE_SKIP; | |
| break; | |
| ua_fail1: | |
| lws_genrsa_destroy(&ctx); | |
| ua_fail: | |
| write_task(pss, NULL, SSH_WT_UA_FAILURE); | |
| ua_fail_silently: | |
| lws_ua_destroy(pss); | |
| /* Sect 4, RFC4252 | |
| * | |
| * Additionally, the implementation SHOULD limit the | |
| * number of failed authentication attempts a client | |
| * may perform in a single session (the RECOMMENDED | |
| * limit is 20 attempts). If the threshold is | |
| * exceeded, the server SHOULD disconnect. | |
| */ | |
| if (pss->count_auth_attempts++ > 20) | |
| goto bail; | |
| pss->parser_state = SSH_KEX_STATE_SKIP; | |
| break; | |
| } | |
| pss->pos++; | |
| } | |
| return 0; | |
| bail: | |
| lws_kex_destroy(pss); | |
| lws_ua_destroy(pss); | |
| return SSH_DISCONNECT_KEY_EXCHANGE_FAILED; | |
| } | |
| static int | |
| parse(struct per_session_data__sshd *pss, uint8_t *p, size_t len) | |
| { | |
| while (len--) { | |
| if (pss->copy_to_I_C && pss->kex->I_C_payload_len < | |
| pss->kex->I_C_alloc_len && | |
| pss->parser_state != SSHS_MSG_EAT_PADDING) | |
| pss->kex->I_C[pss->kex->I_C_payload_len++] = *p; | |
| if (pss->active_keys_cts.valid && | |
| pss->parser_state == SSHS_MSG_LEN) | |
| /* take a copy for full decrypt */ | |
| pss->packet_assembly[pss->pa_pos++] = *p; | |
| if (pss->active_keys_cts.valid && | |
| pss->parser_state == SSHS_MSG_PADDING && | |
| pss->msg_len) { | |
| /* we are going to have to decrypt it */ | |
| uint32_t cp, l = pss->msg_len + 4 + | |
| pss->active_keys_cts.MAC_length; | |
| uint8_t pt[2048]; | |
| len++; | |
| cp = (uint32_t)len; | |
| if (cp > l - pss->pa_pos) | |
| cp = l - pss->pa_pos; | |
| if (cp > sizeof(pss->packet_assembly) - | |
| pss->pa_pos) { | |
| lwsl_err("Packet is too big to decrypt\n"); | |
| goto bail; | |
| } | |
| if (pss->msg_len < 2 + 4) { | |
| lwsl_err("packet too small\n"); | |
| goto bail; | |
| } | |
| memcpy(&pss->packet_assembly[pss->pa_pos], p, cp); | |
| pss->pa_pos += cp; | |
| len -= cp; | |
| p += cp; | |
| if (pss->pa_pos != l) | |
| return 0; | |
| /* decrypt it */ | |
| cp = lws_chacha_decrypt(&pss->active_keys_cts, | |
| pss->ssh_sequence_ctr_cts++, | |
| pss->packet_assembly, | |
| pss->pa_pos, pt); | |
| if (cp) { | |
| lwsl_notice("Decryption failed: %d\n", cp); | |
| goto bail; | |
| } | |
| if (lws_ssh_parse_plaintext(pss, pt + 4, pss->msg_len)) | |
| goto bail; | |
| pss->pa_pos = 0; | |
| pss->ctr = 0; | |
| continue; | |
| } | |
| if (lws_ssh_parse_plaintext(pss, p, 1)) | |
| goto bail; | |
| p++; | |
| } | |
| return 0; | |
| bail: | |
| lws_kex_destroy(pss); | |
| lws_ua_destroy(pss); | |
| return SSH_DISCONNECT_KEY_EXCHANGE_FAILED; | |
| } | |
| static uint32_t | |
| pad_and_encrypt(uint8_t *dest, void *ps, uint8_t *pp, | |
| struct per_session_data__sshd *pss, int skip_pad) | |
| { | |
| uint32_t n; | |
| if (!skip_pad) | |
| lws_pad_set_length(pss, ps, &pp, &pss->active_keys_stc); | |
| n = lws_ptr_diff(pp, ps); | |
| if (!pss->active_keys_stc.valid) { | |
| memcpy(dest, ps, n); | |
| return n; | |
| } | |
| lws_chacha_encrypt(&pss->active_keys_stc, pss->ssh_sequence_ctr_stc, | |
| ps, n, dest); | |
| n += pss->active_keys_stc.MAC_length; | |
| return n; | |
| } | |
| static int | |
| lws_callback_raw_sshd(struct lws *wsi, enum lws_callback_reasons reason, | |
| void *user, void *in, size_t len) | |
| { | |
| struct per_session_data__sshd *pss = | |
| (struct per_session_data__sshd *)user, **p; | |
| struct per_vhost_data__sshd *vhd = NULL; | |
| uint8_t buf[LWS_PRE + 1024], *pp, *ps = &buf[LWS_PRE + 512], *ps1 = NULL; | |
| const struct lws_protocol_vhost_options *pvo; | |
| const struct lws_protocols *prot; | |
| struct lws_ssh_channel *ch; | |
| char lang[10]; | |
| int n, m, o; | |
| /* | |
| * Because we are an abstract protocol plugin, we will get called by | |
| * wsi that actually bind to a plugin "on top of us" that calls thru | |
| * to our callback. | |
| * | |
| * Under those circumstances, we can't simply get a pointer to our own | |
| * protocol from the wsi. If there's a pss already, we can get it from | |
| * there, but the first time for each connection we have to look it up. | |
| */ | |
| if (pss && pss->vhd) | |
| vhd = (struct per_vhost_data__sshd *) | |
| lws_protocol_vh_priv_get(lws_get_vhost(wsi), | |
| pss->vhd->protocol); | |
| else | |
| if (lws_get_vhost(wsi)) | |
| vhd = (struct per_vhost_data__sshd *) | |
| lws_protocol_vh_priv_get(lws_get_vhost(wsi), | |
| lws_vhost_name_to_protocol( | |
| lws_get_vhost(wsi), "lws-ssh-base")); | |
| switch ((int)reason) { | |
| case LWS_CALLBACK_PROTOCOL_INIT: | |
| vhd = lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi), | |
| lws_get_protocol(wsi), | |
| sizeof(struct per_vhost_data__sshd)); | |
| vhd->context = lws_get_context(wsi); | |
| vhd->protocol = lws_get_protocol(wsi); | |
| vhd->vhost = lws_get_vhost(wsi); | |
| pvo = (const struct lws_protocol_vhost_options *)in; | |
| while (pvo) { | |
| /* | |
| * the user code passes the ops struct address to us | |
| * using a pvo (per-vhost option) | |
| */ | |
| if (!strcmp(pvo->name, "ops")) | |
| vhd->ops = (const struct lws_ssh_ops *)pvo->value; | |
| /* | |
| * the user code is telling us to get the ops struct | |
| * from another protocol's protocol.user pointer | |
| */ | |
| if (!strcmp(pvo->name, "ops-from")) { | |
| prot = lws_vhost_name_to_protocol(vhd->vhost, | |
| pvo->value); | |
| if (prot) | |
| vhd->ops = (const struct lws_ssh_ops *)prot->user; | |
| else | |
| lwsl_err("%s: can't find protocol %s\n", | |
| __func__, pvo->value); | |
| } | |
| pvo = pvo->next; | |
| } | |
| if (!vhd->ops) { | |
| lwsl_err("ssh pvo \"ops\" is mandatory\n"); | |
| return 1; | |
| } | |
| /* | |
| * The user code ops api_version has to be current | |
| */ | |
| if (vhd->ops->api_version != LWS_SSH_OPS_VERSION) { | |
| lwsl_err("FATAL ops is api_version v%d but code is v%d\n", | |
| vhd->ops->api_version, LWS_SSH_OPS_VERSION); | |
| return 1; | |
| } | |
| break; | |
| case LWS_CALLBACK_RAW_ADOPT: | |
| lwsl_info("LWS_CALLBACK_RAW_ADOPT\n"); | |
| if (!vhd) | |
| return -1; | |
| pss->next = vhd->live_pss_list; | |
| vhd->live_pss_list = pss; | |
| pss->parser_state = SSH_INITIALIZE_TRANSIENT; | |
| pss->wsi = wsi; | |
| pss->vhd = vhd; | |
| pss->kex_state = KEX_STATE_EXPECTING_CLIENT_OFFER; | |
| pss->active_keys_cts.padding_alignment = 8; | |
| pss->active_keys_stc.padding_alignment = 8; | |
| if (lws_kex_create(pss)) | |
| return -1; | |
| write_task(pss, NULL, SSH_WT_VERSION); | |
| /* sect 4 RFC4252 | |
| * | |
| * The server SHOULD have a timeout for authentication and | |
| * disconnect if the authentication has not been accepted | |
| * within the timeout period. | |
| * | |
| * The RECOMMENDED timeout period is 10 minutes. | |
| */ | |
| lws_set_timeout(wsi, | |
| SSH_PENDING_TIMEOUT_CONNECT_TO_SUCCESSFUL_AUTH, 10 * 60); | |
| break; | |
| case LWS_CALLBACK_RAW_CLOSE: | |
| if (!pss) | |
| return -1; | |
| lwsl_info("LWS_CALLBACK_RAW_CLOSE\n"); | |
| lws_kex_destroy(pss); | |
| lws_ua_destroy(pss); | |
| ssh_free_set_NULL(pss->last_alloc); | |
| while (pss->ch_list) | |
| ssh_destroy_channel(pss, pss->ch_list); | |
| lws_chacha_destroy(&pss->active_keys_cts); | |
| lws_chacha_destroy(&pss->active_keys_stc); | |
| p = &vhd->live_pss_list; | |
| while (*p) { | |
| if ((*p) == pss) { | |
| *p = pss->next; | |
| continue; | |
| } | |
| p = &((*p)->next); | |
| } | |
| break; | |
| case LWS_CALLBACK_RAW_RX: | |
| if (!pss) | |
| return -1; | |
| if (parse(pss, in, len)) | |
| return -1; | |
| break; | |
| case LWS_CALLBACK_RAW_WRITEABLE: | |
| if (!pss) | |
| break; | |
| n = 0; | |
| o = pss->write_task[pss->wt_tail]; | |
| ch = pss->write_channel[pss->wt_tail]; | |
| if (pss->wt_head == pss->wt_tail) | |
| o = SSH_WT_NONE; | |
| switch (o) { | |
| case SSH_WT_VERSION: | |
| if (!pss->vhd) | |
| break; | |
| n = lws_snprintf((char *)buf + LWS_PRE, | |
| sizeof(buf) - LWS_PRE - 1, "%s\r\n", | |
| pss->vhd->ops->server_string); | |
| write_task(pss, NULL, SSH_WT_OFFER); | |
| break; | |
| case SSH_WT_OFFER: | |
| if (!pss->vhd) | |
| break; | |
| m = 0; | |
| n = offer(pss, buf + LWS_PRE, | |
| sizeof(buf) - LWS_PRE, 0, &m); | |
| if (n == 0) { | |
| lwsl_notice("Too small\n"); | |
| return -1; | |
| } | |
| if (!pss->kex) { | |
| lwsl_notice("%s: SSH_WT_OFFER: pss->kex is NULL\n", | |
| __func__); | |
| return -1; | |
| } | |
| /* we need a copy of it to generate the hash later */ | |
| if (pss->kex->I_S) | |
| free(pss->kex->I_S); | |
| pss->kex->I_S = sshd_zalloc(m); | |
| if (!pss->kex->I_S) { | |
| lwsl_notice("OOM 5: %d\n", m); | |
| return -1; | |
| } | |
| /* without length + padcount part */ | |
| memcpy(pss->kex->I_S, buf + LWS_PRE + 5, m); | |
| pss->kex->I_S_payload_len = m; /* without padding */ | |
| break; | |
| case SSH_WT_OFFER_REPLY: | |
| memcpy(ps, pss->kex->kex_r, pss->kex->kex_r_len); | |
| n = pad_and_encrypt(&buf[LWS_PRE], ps, | |
| ps + pss->kex->kex_r_len, pss, 1); | |
| pss->kex_state = KEX_STATE_REPLIED_TO_OFFER; | |
| /* afterwards, must do newkeys */ | |
| write_task(pss, NULL, SSH_WT_SEND_NEWKEYS); | |
| break; | |
| case SSH_WT_SEND_NEWKEYS: | |
| pp = ps + 5; | |
| *pp++ = SSH_MSG_NEWKEYS; | |
| goto pac; | |
| case SSH_WT_UA_ACCEPT: | |
| /* | |
| * If the server supports the service (and permits | |
| * the client to use it), it MUST respond with the | |
| * following: | |
| * | |
| * byte SSH_MSG_SERVICE_ACCEPT | |
| * string service name | |
| */ | |
| pp = ps + 5; | |
| *pp++ = SSH_MSG_SERVICE_ACCEPT; | |
| lws_p32(pp, pss->npos); | |
| pp += 4; | |
| strcpy((char *)pp, pss->name); | |
| pp += pss->npos; | |
| goto pac; | |
| case SSH_WT_UA_FAILURE: | |
| pp = ps + 5; | |
| *pp++ = SSH_MSG_USERAUTH_FAILURE; | |
| lws_p32(pp, 9); | |
| pp += 4; | |
| strcpy((char *)pp, "publickey"); | |
| pp += 9; | |
| *pp++ = 0; | |
| goto pac; | |
| case SSH_WT_UA_BANNER: | |
| pp = ps + 5; | |
| *pp++ = SSH_MSG_USERAUTH_BANNER; | |
| if (pss->vhd && pss->vhd->ops->banner) | |
| n = (int)pss->vhd->ops->banner((char *)&buf[650], | |
| 150 - 1, | |
| lang, (int)sizeof(lang)); | |
| lws_p32(pp, n); | |
| pp += 4; | |
| strcpy((char *)pp, (char *)&buf[650]); | |
| pp += n; | |
| if (lws_cstr(&pp, lang, sizeof(lang))) | |
| goto bail; | |
| goto pac; | |
| case SSH_WT_UA_PK_OK: | |
| /* | |
| * The server MUST respond to this message with | |
| * either SSH_MSG_USERAUTH_FAILURE or with the | |
| * following: | |
| * | |
| * byte SSH_MSG_USERAUTH_PK_OK | |
| * string public key alg name from the request | |
| * string public key blob from the request | |
| */ | |
| n = 74 + pss->ua->pubkey_len; | |
| if (n > (int)sizeof(buf) - LWS_PRE) { | |
| lwsl_notice("pubkey too large\n"); | |
| goto bail; | |
| } | |
| ps1 = sshd_zalloc(n); | |
| if (!ps1) | |
| goto bail; | |
| ps = ps1; | |
| pp = ps1 + 5; | |
| *pp++ = SSH_MSG_USERAUTH_PK_OK; | |
| if (lws_cstr(&pp, pss->ua->alg, 64)) { | |
| free(ps1); | |
| goto bail; | |
| } | |
| lws_p32(pp, pss->ua->pubkey_len); | |
| pp += 4; | |
| memcpy(pp, pss->ua->pubkey, pss->ua->pubkey_len); | |
| pp += pss->ua->pubkey_len; | |
| /* we no longer need the UA now we judged it */ | |
| lws_ua_destroy(pss); | |
| goto pac; | |
| case SSH_WT_UA_SUCCESS: | |
| pp = ps + 5; | |
| *pp++ = SSH_MSG_USERAUTH_SUCCESS; | |
| /* end SSH_PENDING_TIMEOUT_CONNECT_TO_SUCCESSFUL_AUTH */ | |
| lws_set_timeout(wsi, NO_PENDING_TIMEOUT, 0); | |
| goto pac; | |
| case SSH_WT_CH_OPEN_CONF: | |
| pp = ps + 5; | |
| *pp++ = SSH_MSG_CHANNEL_OPEN_CONFIRMATION; | |
| lws_p32(pp, pss->ch_temp->server_ch); | |
| pp += 4; | |
| lws_p32(pp, pss->ch_temp->sender_ch); | |
| pp += 4; | |
| /* tx initial window size towards us */ | |
| lws_p32(pp, LWS_SSH_INITIAL_WINDOW); | |
| pp += 4; | |
| /* maximum packet size towards us */ | |
| lws_p32(pp, 800); | |
| pp += 4; | |
| lwsl_info("SSH_WT_CH_OPEN_CONF\n"); | |
| /* it's on the linked-list */ | |
| pss->ch_temp = NULL; | |
| goto pac; | |
| case SSH_WT_CH_FAILURE: | |
| pp = ps + 5; | |
| *pp++ = SSH_MSG_CHANNEL_OPEN_FAILURE; | |
| lws_p32(pp, ch->server_ch); | |
| pp += 4; | |
| lws_p32(pp, ch->sender_ch); | |
| pp += 4; | |
| lws_cstr(&pp, "reason", 64); | |
| lws_cstr(&pp, "en/US", 64); | |
| lwsl_info("SSH_WT_CH_FAILURE\n"); | |
| goto pac; | |
| case SSH_WT_CHRQ_SUCC: | |
| pp = ps + 5; | |
| *pp++ = SSH_MSG_CHANNEL_SUCCESS; | |
| lws_p32(pp, ch->server_ch); | |
| lwsl_info("SSH_WT_CHRQ_SUCC\n"); | |
| pp += 4; | |
| goto pac; | |
| case SSH_WT_CHRQ_FAILURE: | |
| pp = ps + 5; | |
| *pp++ = SSH_MSG_CHANNEL_FAILURE; | |
| lws_p32(pp, ch->server_ch); | |
| pp += 4; | |
| lwsl_info("SSH_WT_CHRQ_FAILURE\n"); | |
| goto pac; | |
| case SSH_WT_CH_CLOSE: | |
| pp = ps + 5; | |
| *pp++ = SSH_MSG_CHANNEL_CLOSE; | |
| lws_p32(pp, ch->server_ch); | |
| lwsl_info("SSH_WT_CH_CLOSE\n"); | |
| pp += 4; | |
| goto pac; | |
| case SSH_WT_CH_EOF: | |
| pp = ps + 5; | |
| *pp++ = SSH_MSG_CHANNEL_EOF; | |
| lws_p32(pp, ch->server_ch); | |
| lwsl_info("SSH_WT_CH_EOF\n"); | |
| pp += 4; | |
| goto pac; | |
| case SSH_WT_SCP_ACK_ERROR: | |
| case SSH_WT_SCP_ACK_OKAY: | |
| pp = ps + 5; | |
| *pp++ = SSH_MSG_CHANNEL_DATA; | |
| /* ps + 6 */ | |
| lws_p32(pp, ch->sender_ch); | |
| pp += 4; | |
| lws_p32(pp, 1); | |
| pp += 4; | |
| if (o == SSH_WT_SCP_ACK_ERROR) | |
| *pp++ = 2; | |
| else | |
| *pp++ = 0; | |
| lwsl_info("SSH_WT_SCP_ACK_OKAY\n"); | |
| goto pac; | |
| case SSH_WT_WINDOW_ADJUST: | |
| pp = ps + 5; | |
| *pp++ = SSH_MSG_CHANNEL_WINDOW_ADJUST; | |
| /* ps + 6 */ | |
| lws_p32(pp, ch->sender_ch); | |
| pp += 4; | |
| lws_p32(pp, 32768); | |
| pp += 4; | |
| lwsl_info("send SSH_MSG_CHANNEL_WINDOW_ADJUST\n"); | |
| goto pac; | |
| case SSH_WT_EXIT_STATUS: | |
| pp = ps + 5; | |
| *pp++ = SSH_MSG_CHANNEL_REQUEST; | |
| lws_p32(pp, ch->sender_ch); | |
| pp += 4; | |
| lws_p32(pp, 11); | |
| pp += 4; | |
| strcpy((char *)pp, "exit-status"); | |
| pp += 11; | |
| *pp++ = 0; | |
| lws_p32(pp, ch->retcode); | |
| pp += 4; | |
| lwsl_info("send SSH_MSG_CHANNEL_EXIT_STATUS\n"); | |
| goto pac; | |
| case SSH_WT_NONE: | |
| default: | |
| /* sending payload */ | |
| ch = ssh_get_server_ch(pss, 0); | |
| /* have a channel up to send on? */ | |
| if (!ch) | |
| break; | |
| if (!pss->vhd || !pss->vhd->ops) | |
| break; | |
| n = pss->vhd->ops->tx_waiting(ch->priv); | |
| if (n < 0) | |
| return -1; | |
| if (!n) | |
| /* nothing to send */ | |
| break; | |
| if (n == (LWS_STDOUT | LWS_STDERR)) { | |
| /* pick one using round-robin */ | |
| if (pss->serviced_stderr_last) | |
| n = LWS_STDOUT; | |
| else | |
| n = LWS_STDERR; | |
| } | |
| pss->serviced_stderr_last = !!(n & LWS_STDERR); | |
| /* stdout or stderr */ | |
| pp = ps + 5; | |
| if (n == LWS_STDOUT) | |
| *pp++ = SSH_MSG_CHANNEL_DATA; | |
| else | |
| *pp++ = SSH_MSG_CHANNEL_EXTENDED_DATA; | |
| /* ps + 6 */ | |
| lws_p32(pp, pss->ch_list->server_ch); | |
| m = 14; | |
| if (n == LWS_STDERR) { | |
| pp += 4; | |
| /* data type code... 1 for stderr payload */ | |
| lws_p32(pp, SSH_EXTENDED_DATA_STDERR); | |
| m = 18; | |
| } | |
| /* also skip another strlen u32 at + 10 / +14 */ | |
| pp += 8; | |
| /* ps + 14 / + 18 */ | |
| pp += pss->vhd->ops->tx(ch->priv, n, pp, | |
| &buf[sizeof(buf) - 1] - pp); | |
| lws_p32(ps + m - 4, lws_ptr_diff(pp, (ps + m))); | |
| if (pss->vhd->ops->tx_waiting(ch->priv) > 0) | |
| lws_callback_on_writable(wsi); | |
| ch->window -= lws_ptr_diff(pp, ps) - m; | |
| //lwsl_debug("our send window: %d\n", ch->window); | |
| /* fallthru */ | |
| pac: | |
| if (!pss->vhd) | |
| break; | |
| n = pad_and_encrypt(&buf[LWS_PRE], ps, pp, pss, 0); | |
| break; | |
| bail: | |
| lws_ua_destroy(pss); | |
| lws_kex_destroy(pss); | |
| return 1; | |
| } | |
| if (n > 0) { | |
| m = lws_write(wsi, (unsigned char *)buf + LWS_PRE, n, | |
| LWS_WRITE_HTTP); | |
| switch(o) { | |
| case SSH_WT_SEND_NEWKEYS: | |
| lwsl_info("Activating STC keys\n"); | |
| pss->active_keys_stc = pss->kex->keys_next_stc; | |
| lws_chacha_activate(&pss->active_keys_stc); | |
| pss->kex_state = KEX_STATE_CRYPTO_INITIALIZED; | |
| pss->kex->newkeys |= 1; | |
| if (pss->kex->newkeys == 3) | |
| lws_kex_destroy(pss); | |
| break; | |
| case SSH_WT_UA_PK_OK: | |
| free(ps1); | |
| break; | |
| case SSH_WT_CH_CLOSE: | |
| if (ch->received_close) { | |
| /* | |
| * We are sending this at the behest of | |
| * the remote peer... | |
| * we can destroy the channel with no | |
| * further communication. | |
| */ | |
| ssh_destroy_channel(pss, ch); | |
| break; | |
| } | |
| ch->sent_close = 1; | |
| break; | |
| } | |
| if (m < 0) { | |
| lwsl_err("ERR %d from write\n", m); | |
| goto bail; | |
| } | |
| if (o != SSH_WT_VERSION) | |
| pss->ssh_sequence_ctr_stc++; | |
| if (o != SSH_WT_NONE) | |
| pss->wt_tail = | |
| (pss->wt_tail + 1) & 7; | |
| } else | |
| if (o == SSH_WT_UA_PK_OK) /* free it either way */ | |
| free(ps1); | |
| ch = ssh_get_server_ch(pss, 0); | |
| if (pss->wt_head != pss->wt_tail || | |
| (ch && ch->priv && pss->vhd && | |
| pss->vhd->ops->tx_waiting(ch->priv))) | |
| lws_callback_on_writable(wsi); | |
| break; | |
| case LWS_CALLBACK_SSH_UART_SET_RXFLOW: | |
| /* | |
| * this is sent to set rxflow state on any connections that | |
| * sink on a particular sink. The sink index affected is in len | |
| * | |
| * More than one protocol may sink to the same uart, and the | |
| * protocol may select the sink itself, eg, in the URL used | |
| * to set up the connection. | |
| */ | |
| lwsl_notice("sshd LWS_CALLBACK_SSH_UART_SET_RXFLOW: wsi %p, %d\n", | |
| wsi, (int)len & 1); | |
| lws_rx_flow_control(wsi, len & 1); | |
| break; | |
| case LWS_CALLBACK_CGI: | |
| if (!pss) | |
| break; | |
| if (pss->vhd && pss->vhd->ops && | |
| pss->vhd->ops->child_process_io && | |
| pss->vhd->ops->child_process_io(pss->ch_temp->priv, | |
| pss->wsi, (struct lws_cgi_args *)in)) | |
| return -1; | |
| break; | |
| case LWS_CALLBACK_CGI_PROCESS_ATTACH: | |
| if (!pss) | |
| break; | |
| ch = ssh_get_server_ch(pss, pss->channel_doing_spawn); | |
| if (ch) { | |
| ch->spawn_pid = (int)len; /* child process PID */ | |
| lwsl_notice("associated PID %d to ch %d\n", (int)len, | |
| pss->channel_doing_spawn); | |
| } | |
| break; | |
| case LWS_CALLBACK_CGI_TERMINATED: | |
| if (!pss) | |
| break; | |
| if (pss->vhd && pss->vhd->ops && | |
| pss->vhd->ops->child_process_terminated) | |
| pss->vhd->ops->child_process_terminated(pss->ch_temp->priv, | |
| pss->wsi); | |
| /* | |
| * we have the child PID in len... we need to match it to a | |
| * channel that is on the wsi | |
| */ | |
| ch = pss->ch_list; | |
| while (ch) { | |
| if (ch->spawn_pid == len) { | |
| lwsl_notice("starting close of ch with PID %d\n", | |
| (int)len); | |
| ch->scheduled_close = 1; | |
| write_task(pss, ch, SSH_WT_CH_CLOSE); | |
| break; | |
| } | |
| ch = ch->next; | |
| } | |
| break; | |
| default: | |
| break; | |
| } | |
| return 0; | |
| } | |
| #define LWS_PLUGIN_PROTOCOL_LWS_RAW_SSHD { \ | |
| "lws-ssh-base", \ | |
| lws_callback_raw_sshd, \ | |
| sizeof(struct per_session_data__sshd), \ | |
| 1024, 0, NULL, 900 \ | |
| } | |
| const struct lws_protocols protocols_sshd[] = { | |
| LWS_PLUGIN_PROTOCOL_LWS_RAW_SSHD, | |
| { NULL, NULL, 0, 0, 0, NULL, 0 } /* terminator */ | |
| }; | |
| #if !defined (LWS_PLUGIN_STATIC) | |
| LWS_VISIBLE const lws_plugin_protocol_t lws_ssh_base = { | |
| .hdr = { | |
| "ssh base", | |
| "lws_protocol_plugin", | |
| LWS_PLUGIN_API_MAGIC | |
| }, | |
| .protocols = protocols_sshd, | |
| .count_protocols = LWS_ARRAY_SIZE(protocols_sshd), | |
| .extensions = NULL, | |
| .count_extensions = 0, | |
| }; | |
| #endif |