You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
483 lines
11 KiB
483 lines
11 KiB
/*
|
|
* ws protocol handler plugin for sshd demo
|
|
*
|
|
* Written in 2010-2019 by Andy Green <andy@warmcat.com>
|
|
*
|
|
* This file is made available under the Creative Commons CC0 1.0
|
|
* Universal Public Domain Dedication.
|
|
*
|
|
* The person who associated a work with this deed has dedicated
|
|
* the work to the public domain by waiving all of his or her rights
|
|
* to the work worldwide under copyright law, including all related
|
|
* and neighboring rights, to the extent allowed by law. You can copy,
|
|
* modify, distribute and perform the work, even for commercial purposes,
|
|
* all without asking permission.
|
|
*
|
|
* These test plugins are intended to be adapted for use in your code, which
|
|
* may be proprietary. So unlike the library itself, they are licensed
|
|
* Public Domain.
|
|
*/
|
|
|
|
#if !defined (LWS_PLUGIN_STATIC)
|
|
#define LWS_DLL
|
|
#define LWS_INTERNAL
|
|
#include <libwebsockets.h>
|
|
#endif
|
|
|
|
#include <lws-ssh.h>
|
|
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <errno.h>
|
|
|
|
#define TEST_SERVER_KEY_PATH "/etc/lws-test-sshd-server-key"
|
|
|
|
struct per_vhost_data__lws_sshd_demo {
|
|
const struct lws_protocols *ssh_base_protocol;
|
|
int privileged_fd;
|
|
};
|
|
|
|
/*
|
|
* This is a copy of the lws ssh test public key, you can find it in
|
|
* /usr[/local]/share/libwebsockets-test-server/lws-ssh-test-keys.pub
|
|
* and the matching private key there too in .../lws-ssh-test-keys
|
|
*
|
|
* If the vhost with this protocol is using localhost:2222, you can test with
|
|
* the matching private key like this:
|
|
*
|
|
* ssh -p 2222 -i /usr/local/share/libwebsockets-test-server/lws-ssh-test-keys anyuser@127.0.0.1
|
|
*
|
|
* These keys are distributed for testing! Don't use them on a real system
|
|
* unless you want anyone with a copy of lws to access it.
|
|
*/
|
|
static const char *authorized_key =
|
|
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCnWiP+c+kSD6Lk+C6NA9KruApa45sbt"
|
|
"94/dxT0bCITlAA/+PBk6mR1lwWgXYozOMdrHrqx34piqDyXnc4HabqCaOm/FrYhkCPL8z"
|
|
"a26PMYqteSosuwKv//5iT6ZWhNnsMwExBwtV6MIq0MxAeWqxRnYNWpNM8iN6sFzkdG/YF"
|
|
"dyHrIBTgwzM77NLCMl6GEkJErRCFppC2SwYxGa3BRrgUwX3LkV8HpMIaYHFo1Qgj7Scqm"
|
|
"HwS2R75SOqi2aOWDpKjznARg9JgzDWSQi4seBMV2oL0BTwJANSDf+p0sQLsaKGJhpVpBQ"
|
|
"yS2wUeyuGyytupWzEluQrajMZq52iotcogv5BfeulfTTFbJP4kuHOsSP0lsQ2lpMDQANS"
|
|
"HEvXxzHJLDLXM9gXJzwJ+ZiRt6R+bfmP1nfN3MiWtxcIbBanWwQK6xTCKBe4wPaKta5EU"
|
|
"6wsLPeakOIVzoeaOu/HsbtPZlwX0Mu/oUFcfKyKAhlkU15MOAIEfUPo8Yh52bWMlIlpZa"
|
|
"4xWbLMGw3GrsrPPdcsAauyqvY4/NjjWQbWhP1SuUfvv5709PIiOUjVKK2HUwmR1ouch6X"
|
|
"MQGXfMR1h1Wjvc+bkNs17gCIrQnFilAZLC3Sm3Opiz/4LO99Hw448G0RM2vQn0mJE46w"
|
|
"Eu/B10U6Jf4Efojhh1dk85BD1LTIb+N3Q== ssh-test-key@lws";
|
|
|
|
enum states {
|
|
SSH_TEST_GREET,
|
|
SSH_TEST_PRESSED,
|
|
SSH_TEST_DONE,
|
|
};
|
|
|
|
static const char * const strings[] =
|
|
{
|
|
/* SSH_TEST_GREET */
|
|
"Thanks for logging to lws sshd server demo.\n\r"
|
|
"\n\r"
|
|
"This demo is very simple, it waits for you to press\n\r"
|
|
"a key, and acknowledges it. Then press another key\n\r"
|
|
"and it will exit. But actually that demos the basic\n\r"
|
|
"sshd functions underneath. You can use the ops struct\n\r"
|
|
"members to add a pty / shell or whatever you want.\n\r"
|
|
"\n\r"
|
|
"Press a key...\n\r",
|
|
|
|
/* SSH_TEST_PRESSED */
|
|
"Thanks for pressing a key. Press another to exit.\n\r",
|
|
|
|
/* SSH_TEST_DONE */
|
|
"Bye!\n\r"
|
|
};
|
|
|
|
struct sshd_instance_priv {
|
|
struct lws *wsi;
|
|
enum states state;
|
|
const char *ptr;
|
|
int pos;
|
|
int len;
|
|
};
|
|
|
|
static void
|
|
enter_state(struct sshd_instance_priv *priv, enum states state)
|
|
{
|
|
priv->state = state;
|
|
priv->ptr = strings[state];
|
|
priv->pos = 0;
|
|
priv->len = (int)strlen(priv->ptr);
|
|
|
|
lws_callback_on_writable(priv->wsi);
|
|
}
|
|
|
|
/* ops: channel lifecycle */
|
|
|
|
static int
|
|
ssh_ops_channel_create(struct lws *wsi, void **_priv)
|
|
{
|
|
struct sshd_instance_priv *priv;
|
|
|
|
priv = malloc(sizeof(struct sshd_instance_priv));
|
|
*_priv = priv;
|
|
if (!priv)
|
|
return 1;
|
|
|
|
memset(priv, 0, sizeof(*priv));
|
|
priv->wsi = wsi;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
ssh_ops_channel_destroy(void *_priv)
|
|
{
|
|
struct sshd_instance_priv *priv = _priv;
|
|
|
|
free(priv);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* ops: IO */
|
|
|
|
static int
|
|
ssh_ops_tx_waiting(void *_priv)
|
|
{
|
|
struct sshd_instance_priv *priv = _priv;
|
|
|
|
if (priv->state == SSH_TEST_DONE &&
|
|
priv->pos == priv->len)
|
|
return -1; /* exit */
|
|
|
|
if (priv->pos != priv->len)
|
|
return LWS_STDOUT;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static size_t
|
|
ssh_ops_tx(void *_priv, int stdch, uint8_t *buf, size_t len)
|
|
{
|
|
struct sshd_instance_priv *priv = _priv;
|
|
size_t chunk = len;
|
|
|
|
if (stdch != LWS_STDOUT)
|
|
return 0;
|
|
|
|
if ((size_t)(priv->len - priv->pos) < chunk)
|
|
chunk = priv->len - priv->pos;
|
|
|
|
if (!chunk)
|
|
return 0;
|
|
|
|
memcpy(buf, priv->ptr + priv->pos, chunk);
|
|
priv->pos += (int)chunk;
|
|
|
|
if (priv->state == SSH_TEST_DONE && priv->pos == priv->len) {
|
|
/*
|
|
* we are sending the last thing we want to send
|
|
* before exiting. Make it ask again at ssh_ops_tx_waiting()
|
|
* and we will exit then, after this has been sent
|
|
*/
|
|
lws_callback_on_writable(priv->wsi);
|
|
}
|
|
|
|
return chunk;
|
|
}
|
|
|
|
|
|
static int
|
|
ssh_ops_rx(void *_priv, struct lws *wsi, const uint8_t *buf, uint32_t len)
|
|
{
|
|
struct sshd_instance_priv *priv = _priv;
|
|
|
|
if (priv->state < SSH_TEST_DONE)
|
|
enter_state(priv, priv->state + 1);
|
|
else
|
|
return -1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* ops: storage for the (autogenerated) persistent server key */
|
|
|
|
static size_t
|
|
ssh_ops_get_server_key(struct lws *wsi, uint8_t *buf, size_t len)
|
|
{
|
|
struct per_vhost_data__lws_sshd_demo *vhd =
|
|
(struct per_vhost_data__lws_sshd_demo *)
|
|
lws_protocol_vh_priv_get(lws_get_vhost(wsi),
|
|
lws_get_protocol(wsi));
|
|
int n;
|
|
|
|
if (lseek(vhd->privileged_fd, 0, SEEK_SET) < 0)
|
|
return 0;
|
|
n = read(vhd->privileged_fd, buf, (int)len);
|
|
if (n < 0) {
|
|
lwsl_err("%s: read failed: %d\n", __func__, n);
|
|
n = 0;
|
|
}
|
|
|
|
return n;
|
|
}
|
|
|
|
static size_t
|
|
ssh_ops_set_server_key(struct lws *wsi, uint8_t *buf, size_t len)
|
|
{
|
|
struct per_vhost_data__lws_sshd_demo *vhd =
|
|
(struct per_vhost_data__lws_sshd_demo *)
|
|
lws_protocol_vh_priv_get(lws_get_vhost(wsi),
|
|
lws_get_protocol(wsi));
|
|
int n;
|
|
|
|
n = write(vhd->privileged_fd, buf, (int)len);
|
|
if (n < 0) {
|
|
lwsl_err("%s: read failed: %d\n", __func__, errno);
|
|
n = 0;
|
|
}
|
|
|
|
return n;
|
|
}
|
|
|
|
/* ops: auth */
|
|
|
|
static int
|
|
ssh_ops_is_pubkey_authorized(const char *username, const char *type,
|
|
const uint8_t *peer, int peer_len)
|
|
{
|
|
char *aps, *p, *ps;
|
|
int n = (int)strlen(type), alen = 2048, ret = 2, len;
|
|
size_t s = 0;
|
|
|
|
lwsl_info("%s: checking pubkey for %s\n", __func__, username);
|
|
|
|
s = strlen(authorized_key) + 1;
|
|
|
|
aps = malloc(s);
|
|
if (!aps) {
|
|
lwsl_notice("OOM 1\n");
|
|
goto bail_p1;
|
|
}
|
|
memcpy(aps, authorized_key, s);
|
|
|
|
/* we only understand RSA */
|
|
if (strcmp(type, "ssh-rsa")) {
|
|
lwsl_notice("type is not ssh-rsa\n");
|
|
goto bail_p1;
|
|
}
|
|
p = aps;
|
|
|
|
if (strncmp(p, type, n)) {
|
|
lwsl_notice("lead-in string does not match %s\n", type);
|
|
goto bail_p1;
|
|
}
|
|
|
|
p += n;
|
|
if (*p != ' ') {
|
|
lwsl_notice("missing space at end of lead-in\n");
|
|
goto bail_p1;
|
|
}
|
|
|
|
p++;
|
|
ps = malloc(alen);
|
|
if (!ps) {
|
|
lwsl_notice("OOM 2\n");
|
|
free(aps);
|
|
goto bail;
|
|
}
|
|
len = lws_b64_decode_string(p, ps, alen);
|
|
free(aps);
|
|
if (len < 0) {
|
|
lwsl_notice("key too big\n");
|
|
goto bail;
|
|
}
|
|
|
|
if (peer_len > len) {
|
|
lwsl_notice("peer_len %d bigger than decoded len %d\n",
|
|
peer_len, len);
|
|
goto bail;
|
|
}
|
|
|
|
/*
|
|
* once we are past that, it's the same <len32>name
|
|
* <len32>E<len32>N that the peer sends us
|
|
*/
|
|
if (memcmp(peer, ps, peer_len)) {
|
|
lwsl_info("%s: factors mismatch, rejecting key\n", __func__);
|
|
goto bail;
|
|
}
|
|
|
|
lwsl_info("pubkey authorized\n");
|
|
|
|
ret = 0;
|
|
bail:
|
|
free(ps);
|
|
|
|
return ret;
|
|
|
|
bail_p1:
|
|
if (aps)
|
|
free(aps);
|
|
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
ssh_ops_shell(void *_priv, struct lws *wsi, lws_ssh_finish_exec finish, void *finish_handle)
|
|
{
|
|
struct sshd_instance_priv *priv = _priv;
|
|
|
|
/* for this demo, we don't open a real shell */
|
|
|
|
enter_state(priv, SSH_TEST_GREET);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* ops: banner */
|
|
|
|
static size_t
|
|
ssh_ops_banner(char *buf, size_t max_len, char *lang, size_t max_lang_len)
|
|
{
|
|
int n = lws_snprintf(buf, max_len, "\n"
|
|
" |\\---/| lws-ssh Test Server\n"
|
|
" | o_o | SSH Terminal Server\n"
|
|
" \\_^_/ Copyright (C) 2017 Crash Barrier Ltd\n\n");
|
|
|
|
lws_snprintf(lang, max_lang_len, "en/US");
|
|
|
|
return n;
|
|
}
|
|
|
|
static void
|
|
ssh_ops_disconnect_reason(uint32_t reason, const char *desc,
|
|
const char *desc_lang)
|
|
{
|
|
lwsl_notice("DISCONNECT reason 0x%X, %s (lang %s)\n", reason, desc,
|
|
desc_lang);
|
|
}
|
|
|
|
|
|
static const struct lws_ssh_ops ssh_ops = {
|
|
.channel_create = ssh_ops_channel_create,
|
|
.channel_destroy = ssh_ops_channel_destroy,
|
|
.tx_waiting = ssh_ops_tx_waiting,
|
|
.tx = ssh_ops_tx,
|
|
.rx = ssh_ops_rx,
|
|
.get_server_key = ssh_ops_get_server_key,
|
|
.set_server_key = ssh_ops_set_server_key,
|
|
.set_env = NULL,
|
|
.pty_req = NULL,
|
|
.child_process_io = NULL,
|
|
.child_process_terminated = NULL,
|
|
.exec = NULL,
|
|
.shell = ssh_ops_shell,
|
|
.is_pubkey_authorized = ssh_ops_is_pubkey_authorized,
|
|
.banner = ssh_ops_banner,
|
|
.disconnect_reason = ssh_ops_disconnect_reason,
|
|
.server_string = "SSH-2.0-Libwebsockets",
|
|
.api_version = 2,
|
|
};
|
|
|
|
static int
|
|
callback_lws_sshd_demo(struct lws *wsi, enum lws_callback_reasons reason,
|
|
void *user, void *in, size_t len)
|
|
{
|
|
struct per_vhost_data__lws_sshd_demo *vhd =
|
|
(struct per_vhost_data__lws_sshd_demo *)
|
|
lws_protocol_vh_priv_get(lws_get_vhost(wsi),
|
|
lws_get_protocol(wsi));
|
|
|
|
switch (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__lws_sshd_demo));
|
|
/*
|
|
* During this we still have the privs / caps we were started
|
|
* with. So open an fd on the server key, either just for read
|
|
* or for creat / trunc if doesn't exist. This allows us to
|
|
* deal with it down /etc/.. when just after this we will lose
|
|
* the privileges needed to read / write /etc/...
|
|
*/
|
|
vhd->privileged_fd = lws_open(TEST_SERVER_KEY_PATH, O_RDONLY);
|
|
if (vhd->privileged_fd == -1)
|
|
vhd->privileged_fd = lws_open(TEST_SERVER_KEY_PATH,
|
|
O_CREAT | O_TRUNC | O_RDWR, 0600);
|
|
if (vhd->privileged_fd == -1) {
|
|
lwsl_err("%s: Can't open %s\n", __func__,
|
|
TEST_SERVER_KEY_PATH);
|
|
return -1;
|
|
}
|
|
break;
|
|
|
|
case LWS_CALLBACK_PROTOCOL_DESTROY:
|
|
close(vhd->privileged_fd);
|
|
break;
|
|
|
|
case LWS_CALLBACK_VHOST_CERT_AGING:
|
|
break;
|
|
|
|
case LWS_CALLBACK_EVENT_WAIT_CANCELLED:
|
|
break;
|
|
|
|
default:
|
|
if (!vhd->ssh_base_protocol) {
|
|
vhd->ssh_base_protocol = lws_vhost_name_to_protocol(
|
|
lws_get_vhost(wsi),
|
|
"lws-ssh-base");
|
|
if (vhd->ssh_base_protocol)
|
|
user = lws_adjust_protocol_psds(wsi,
|
|
vhd->ssh_base_protocol->per_session_data_size);
|
|
}
|
|
|
|
if (vhd->ssh_base_protocol)
|
|
return vhd->ssh_base_protocol->callback(wsi, reason,
|
|
user, in, len);
|
|
else
|
|
lwsl_notice("can't find lws-ssh-base\n");
|
|
break;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
#define LWS_PLUGIN_PROTOCOL_LWS_SSHD_DEMO \
|
|
{ \
|
|
"lws-sshd-demo", \
|
|
callback_lws_sshd_demo, \
|
|
0, \
|
|
1024, /* rx buf size must be >= permessage-deflate rx size */ \
|
|
0, (void *)&ssh_ops, 0 \
|
|
}
|
|
|
|
#if !defined (LWS_PLUGIN_STATIC)
|
|
|
|
static const struct lws_protocols protocols[] = {
|
|
LWS_PLUGIN_PROTOCOL_LWS_SSHD_DEMO
|
|
};
|
|
|
|
LWS_VISIBLE int
|
|
init_protocol_lws_sshd_demo(struct lws_context *context,
|
|
struct lws_plugin_capability *c)
|
|
{
|
|
if (c->api_magic != LWS_PLUGIN_API_MAGIC) {
|
|
lwsl_err("Plugin API %d, library API %d", LWS_PLUGIN_API_MAGIC,
|
|
c->api_magic);
|
|
return 1;
|
|
}
|
|
|
|
c->protocols = protocols;
|
|
c->count_protocols = LWS_ARRAY_SIZE(protocols);
|
|
c->extensions = NULL;
|
|
c->count_extensions = 0;
|
|
|
|
return 0;
|
|
}
|
|
|
|
LWS_VISIBLE int
|
|
destroy_protocol_lws_sshd_demo(struct lws_context *context)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
#endif
|