view flashii.c @ 0:aa723e3948a4 default tip

*: initial commit awesome
author Paper <paper@tflc.us>
date Tue, 09 Sep 2025 00:29:57 -0400
parents
children
line wrap: on
line source

/*
 * wcc -- a shitty sockchat client
 *
 * Copyright (c) 2025 Paper
 *
 * 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 "flashii.h"

#include <libwebsockets.h>
#include <assert.h>

#define HEARTBEAT_SECONDS (30)

/* internal linked list of messages to send */
struct flashii_internal_msgbuf {
	struct flashii_internal_msgbuf *next;

	char msg[]; /* meat */
};

struct flashii {
	struct lws_context *ctx;
	struct lws *lws;

	struct flashii_internal_msgbuf *msgbuf;

	flashii_msg_recv_spec msg_recv;
	flashii_user_recv_spec user_recv;

	/* user ID, retrieved after auth success */
	char *id;
	/* username */
	char *username;
	/* color */
	/* char *color;  -- don't care */

	/* permissions
	 * odd, they can be separated by \f or ' ',
	 * so if we want to save them we'll have to write mempbrk */

	/* maybe save channel name too? */

	/* maximum message length in unicode characters */
	int32_t max_msg_length;

	time_t last_heartbeat;
	int need_heartbeat;
};

static char *memduptostr(const char *in, size_t len)
{
	char *q = malloc(len + 1);
	if (!q)
		return NULL;

	memcpy(q, in, len);
	q[len] = 0;

	return q;
}

/* converts a string-like thing to i64 */
static int memtoi64(const char *mem, size_t len, int64_t *pr)
{
	size_t i;
	int64_t r;
	int neg;

	if (len <= 0)
		return -1;

	if (!(mem[0] == '-' || (mem[0] >= '0' && mem[0] <= '9')))
		return -1;

	r = 0;

	neg = (mem[0] == '-');

	for (i = (neg ? 1 : 0); i < len; i++) {
		/* FIXME this does absolutely no checking for overflow etc */
		if (!(mem[i] >= '0' && mem[i] <= '9'))
			return -1;

		r *= 10;
		r += (mem[i] - '0');
	}

	if (mem[0] == '-')
		r = -r;

	*pr = r;

	return 0;
}

/* converts a string-like thing to i32 */
static int memtoi32(const char *mem, size_t len, int32_t *pr)
{
	int64_t r;
	if (memtoi64(mem, len, &r) < 0)
		return -1;

	if (r > INT32_MAX || r < INT32_MIN)
		return -1;

	*pr = r;
	return 0;
}

/* converts a binary string to u32
 *
 * e.x.:
 *  '10010' -> 0x09 */
static int memtou32flags(const char *mem, size_t len, uint32_t *pflags)
{
	size_t i;
	uint32_t u;

	if (len >= 32)
		return -1; /* totally funtastic! */

	u = 0;

	for (i = 0; i < len; i++) {
		switch (mem[i]) {
		case '0':
			break;
		case '1':
			u |= (UINT32_C(1) << i);
			break;
		default:
			/* invalid input */
			return -1;
		}
	}

	*pflags = u;

	return 0;
}

/* ------------------------------------------------------------------------ */
/* packet receiving and parsing.
 *
 * packets are generally received as
 *  [type] (optional subtype) (data)
 * hence I've put together a simple filter thing to be able to handle
 * types and subtypes without duplicating a shit ton of code */

struct flashii_receive_packet_filter {
	const char *prefix;
	size_t len;
	int (*func)(struct flashii *fls, const char *in, size_t len);
};

#define FLASHII_RECEIVE_PACKET_FILTER(prefix, func) \
	{ (prefix "\t"), sizeof(prefix), (func) }

static int flashii_receive_packet_filter(struct flashii *fls, const char *in,
	size_t len, struct flashii_receive_packet_filter *filters)
{
	struct flashii_receive_packet_filter *f;

	for (f = filters; f->prefix; f++) {
		if (f->len > len || memcmp(in, f->prefix, f->len))
			continue; /* nope */

		return f->func(fls, in + f->len, len - f->len);
	}

	return -1;
}

/* ------------------------------------------------------------------------ */
/* enumerate over parts of a packet
 * note that whatever returned to the callback is not guaranteed to be
 * NUL terminated; you should treat it simply as a memory buffer */

/* XXX needs to take in delim */
static int flashii_receive_packet_enum(struct flashii *fls, const char *in,
	size_t len,
	int (*cb)(struct flashii *fls,
			const char *in, size_t len, size_t index, void *userdata),
	void *userdata)
{
	size_t index;

	for (index = 0; /* none */; index++) {
		size_t ilen;

		{
			const char *x;
			x = memchr(in, '\t', len);
			ilen = x ? (x - in) : len;
		}

		//printf("%d: %.*s\n", (int)index, (int)ilen, in);

		if (cb(fls, in, ilen, index, userdata) < 0)
			return -1;

		/* hopefully this is right */
		if (ilen + 1 >= len)
			break;

		in = in + ilen + 1;
		len -= ilen + 1;
	}

	return index;
}

/* ------------------------------------------------------------------------ */
/* 0: pong */

static int flashii_receive_packet_pong(struct flashii *fls, const char *in,
	size_t len)
{
	/* nothing to do */
	return 0;
}

/* ------------------------------------------------------------------------ */
/* 1: auth */

static int flashii_receive_auth_cb(struct flashii *fls, const char *in,
	size_t len, size_t index, void *userdata)
{
	switch (index) {
	case 1:
	case 2:
	case 3:
	case 4:
		return 0; /* nothing */
	case 0:
		fls->id = memduptostr(in, len);
		/* user ID */
		return 0;
	case 5:
		if (memtoi32(in, len, &fls->max_msg_length) < 0)
			return -1;

		return 0;
	}

	return -1;

	(void)userdata;
}

static int flashii_receive_packet_auth_success(struct flashii *fls,
	const char *in, size_t len)
{
	return (flashii_receive_packet_enum(fls, in, len, flashii_receive_auth_cb, NULL) != 5) ? -1 : 0;
}

static int flashii_receive_packet_auth(struct flashii *fls, const char *in,
	size_t len)
{
	struct flashii_receive_packet_filter f[] = {
		FLASHII_RECEIVE_PACKET_FILTER("y", flashii_receive_packet_auth_success),
		/* FLASHII_RECEIVE_PACKET_FILTER("n", flashii_receive_packet_auth_failure), */
		/* FLASHII_RECEIVE_PACKET_FILTER("#", flashii_receive_packet_user_add), */
		/* terminate */
		{ 0 }
	};

	return flashii_receive_packet_filter(fls, in, len, f);
}

/* ------------------------------------------------------------------------ */
/* 2: message add */

static int flashii_receive_packet_msg_cb(struct flashii *fls, const char *in,
	size_t len, size_t index, void *userdata)
{
	struct flashii_msg *msg = userdata;

	switch (index) {
	case 0:
		/* UNIX timestamp */
		if (memtoi64(in, len, &msg->timestamp) < 0)
			return -1;
		return 0;
	case 1:
		msg->id = memduptostr(in, len);
		return 0;
	case 2:
		msg->body = memduptostr(in, len);
		return 0;
	case 3:
		msg->msg_id = memduptostr(in, len);
		return 0;
	case 4:
		if (memtou32flags(in, len, &msg->flags) < 0)
			return -1;
		return 0;
	}

	return -1;
}

static int flashii_receive_packet_msg(struct flashii *fls, const char *in,
	size_t len)
{
	struct flashii_msg msg;

	if (flashii_receive_packet_enum(fls, in, len, flashii_receive_packet_msg_cb, &msg) != 4)
		return -1;

	fls->msg_recv(fls, &msg);
	return 0;
}

/* ------------------------------------------------------------------------ */
/* 7: context */

static int flashii_receive_packet_context_user_list_cb(struct flashii *fls,
	const char *in, size_t len, size_t index, void *userdata)
{
	struct flashii_user *user = userdata;

	if (!index)
		return 0; /* ignored */

	switch ((index - 1) % 5) {
	case 0:
		/* user ID */
		user->id = memduptostr(in, len);
		return 0;
	case 1:
		/* user name */
		user->name = memduptostr(in, len);
		return 0;
	case 2:
		/* color */
		return 0;
	case 3:
		/* perms */
		return 0;
	case 4:
		/* visiblility */
		if (memtoi32(in, len, &user->visible) < 0)
			return -1;
		fls->user_recv(fls, user);
		return 0;
	}

	return -1;
}

static int flashii_receive_packet_context_user_list(struct flashii *fls,
	const char *in, size_t len)
{
	struct flashii_user user;

	if (flashii_receive_packet_enum(fls, in, len, flashii_receive_packet_context_user_list_cb, &user) < 0)
		return -1;

	return 0;
}

static int flashii_receive_packet_context_msg_add_cb(struct flashii *fls,
	const char *in, size_t len, size_t index, void *userdata)
{
	struct flashii_msg *msg = userdata;

	switch (index) {
	case 0:
		/* UNIX timestamp */
		if (memtoi64(in, len, &msg->timestamp) < 0)
			return -1;
		return 0;
	case 1:
		msg->id = memduptostr(in, len);
		return 0;
	case 2:
		/* "user name" wtf */
		return 0;
	case 3:
		/* color */
		return 0;
	case 4:
		/* user permissions */
		return 0;
	case 5:
		msg->body = memduptostr(in, len);
		return 0;
	case 6:
		msg->msg_id = memduptostr(in, len);
		return 0;
	case 7:
		/* notifications */
		return 0;
	case 8:
#if 0
		if (memtou32flags(in, len, &msg->flags) < 0)
			return -1;
#endif
		return 0;
	}

	return -1;
}

static int flashii_receive_packet_context_message_add(struct flashii *fls,
	const char *in, size_t len)
{
	struct flashii_msg msg;

	if (flashii_receive_packet_enum(fls, in, len, flashii_receive_packet_context_msg_add_cb, &msg) < 0)
		return -1;

	fls->msg_recv(fls, &msg);
	return 0;
}

static int flashii_receive_packet_context(struct flashii *fls, const char *in,
	size_t len)
{
	struct flashii_receive_packet_filter f[] = {
		FLASHII_RECEIVE_PACKET_FILTER("0", flashii_receive_packet_context_user_list),
		FLASHII_RECEIVE_PACKET_FILTER("1", flashii_receive_packet_context_message_add),
		/* FLASHII_RECEIVE_PACKET_FILTER("2", ...), */
		/* terminate */
		{ 0 }
	};

	return flashii_receive_packet_filter(fls, in, len, f);
}

/* ------------------------------------------------------------------------ */

static int flashii_receive_packet(struct flashii *fls, const char *in,
	size_t len)
{
	struct flashii_receive_packet_filter f[] = {
		FLASHII_RECEIVE_PACKET_FILTER("0", flashii_receive_packet_pong),
		FLASHII_RECEIVE_PACKET_FILTER("1", flashii_receive_packet_auth),
		FLASHII_RECEIVE_PACKET_FILTER("2", flashii_receive_packet_msg),
		FLASHII_RECEIVE_PACKET_FILTER("7", flashii_receive_packet_context),
		/* terminate */
		{ 0 }
	};

	return flashii_receive_packet_filter(fls, in, len, f);
}

/* ------------------------------------------------------------------------ */

static int flashii_send_vpacket(struct flashii *fls, const char *fmt, va_list ap)
{
	/* this uses vsnprintf to assemble the packet and sends it off */
	int r;
	char *s;
	va_list ap2;

	va_copy(ap2, ap);

	r = vsnprintf(NULL, 0, fmt, ap);

	if (r < 0) {
		va_end(ap2);
		return -1;
	}

	s = malloc(LWS_PRE + r + 1);
	if (!s) {
		va_end(ap2);
		return -1;
	}

	r = vsnprintf(s + LWS_PRE, r + 1, fmt, ap2);

	va_end(ap2);

	if (r < 0)
		return -1;

	lws_write(fls->lws, s + LWS_PRE, r, LWS_WRITE_TEXT);
	free(s);

	return 0;
}

static int flashii_send_packet(struct flashii *fls, const char *fmt, ...)
{
	va_list ap;
	int r;

	va_start(ap, fmt);
	r = flashii_send_vpacket(fls, fmt, ap);
	va_end(ap);

	return r;
}

static int flashii_send_ping(struct flashii *fls, const char *id)
{
	assert(id);
	return flashii_send_packet(fls, "0\t%s", id);
}

static int flashii_send_auth(struct flashii *fls, const char *scheme, const char *args)
{
	assert(scheme);
	assert(args);
	return flashii_send_packet(fls, "1\t%s\t%s", scheme, args);
}

static int flashii_send_msg(struct flashii *fls, const char *id, const char *msg)
{
	assert(id);
	assert(msg);
	return flashii_send_packet(fls, "2\t%s\t%s", id, msg);
}

static int flashii_process_msgbuf(struct flashii *fls)
{
	while (fls->msgbuf) {
		struct flashii_internal_msgbuf *tmp;

		tmp = fls->msgbuf;
		flashii_send_msg(fls, fls->id, tmp->msg);
		fls->msgbuf = tmp->next;
		free(tmp);
	}
}

/* libwebsockets calls us back here */
static int flashii_cb(struct lws *wsi, enum lws_callback_reasons reason,
	void *userdata, void *in, size_t len)
{
	struct flashii *fls = (struct flashii *)userdata;
	static const unsigned char token[] = {
#embed "token.txt"
		, 0
	};

	switch (reason) {
	case LWS_CALLBACK_CLIENT_ESTABLISHED:
		lws_callback_on_writable(wsi);
		break;
	case LWS_CALLBACK_CLIENT_RECEIVE:
		/* incoming message -- print it out
		 * we should handle it based on the type */
		flashii_receive_packet(fls, in, len);
		break;
	case LWS_CALLBACK_CLIENT_WRITEABLE:
		if (fls->need_heartbeat) {
			flashii_send_ping(fls, fls->id);
			fls->need_heartbeat = 0;
		}

		/* process the buffer of messages, if any */
		flashii_process_msgbuf(fls);

		flashii_send_auth(fls, "Misuzu", token);
		/* send message here using lws_write */
		break;
	case LWS_CALLBACK_CLIENT_CLOSED:
	case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
		break;
#if 0
	default:
		printf("UNKNOWN REASON: %d\n", reason);
		break;
#endif
	}

	return lws_callback_http_dummy(wsi, reason, userdata, in, len);
}

static const struct lws_protocols protocols[] = {
	{
		"sockchat",
		flashii_cb,
		sizeof(struct flashii),
		0,
	},
	{ 0 } /* terminator */
};

/* ------------------------------------------------------------------------ */

struct flashii *flashii_init(const char *protocol, const char *address,
	uint16_t port, flashii_msg_recv_spec msg_recv,
	flashii_user_recv_spec user_recv)
{
	struct lws_context_creation_info info;
	struct lws_client_connect_info ccinfo;
	struct flashii *fls;

	fls = calloc(1, sizeof(*fls));
	if (!fls)
		return NULL;

	fls->msg_recv = msg_recv;
	fls->user_recv = user_recv;

	lws_context_info_defaults(&info, NULL);
	info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
	info.port = CONTEXT_PORT_NO_LISTEN; /* we do not run any server */
	info.protocols = protocols;

	fls->ctx = lws_create_context(&info);
	if (!fls->ctx) {
		lwsl_err("lws init failed\n");
		free(fls);
		return NULL;
	}

	/* konekuto */
	memset(&ccinfo, 0, sizeof(ccinfo));
	
	ccinfo.context = fls->ctx;
	ccinfo.address = address;
	ccinfo.port = port;
	ccinfo.path = "/";
	ccinfo.host = address;
	ccinfo.origin = address;
	ccinfo.protocol = "sockchat";
	ccinfo.ssl_connection = LCCSCF_USE_SSL;
	ccinfo.userdata = fls;

	fls->lws = lws_client_connect_via_info(&ccinfo);
	if (!fls->lws) {
		lwsl_cx_err(fls->ctx, "lws connect failed\n");
		lws_context_destroy(fls->ctx);
		free(fls);
		return NULL;
	}

	return fls;
}

/* worker
 *
 * this should be called in an event loop, on the same thread that
 * initialized the structure.
 *
 * `noblock` is a boolean saying... whether we should block :) */
void flashii_work(struct flashii *fls, int noblock)
{
	time_t thetime;

	time(&thetime);

	if (fls->id) {
		if (difftime(thetime, fls->last_heartbeat) >= 30.0) {
			fls->need_heartbeat = 1;
			fls->last_heartbeat = thetime;
			lws_callback_on_writable(fls->lws);
		}
	}

	lws_service(fls->ctx, (noblock) ? -1 : 0);
}

int flashii_send_message(struct flashii *fls, const char *msg)
{
	/* append to the internal linked list of messages */
	size_t len = strlen(msg);

	struct flashii_internal_msgbuf *msgbuf = malloc(sizeof(*msgbuf) + len + 1);
	if (!msgbuf)
		return -1; /* WHAT */

	msgbuf->next = NULL;

	/* copy it allll in (includes NUL terminator) */
	memcpy(msgbuf->msg, msg, len + 1);

	if (fls->msgbuf) {
		struct flashii_internal_msgbuf *tail = fls->msgbuf;
		while (tail->next)
			tail = tail->next;
		tail->next = msgbuf;
	} else {
		fls->msgbuf = msgbuf;
	}

	lws_callback_on_writable(fls->lws);

	return 0; /* yay */
}