pkgsrc-Bugs archive

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index][Old Index]

pkg/60707: new pkgsrc cwebsockify written in glib/C faster than then original (pytthon) websockify



>Number:         60707
>Category:       pkg
>Synopsis:       new pkgsrc cwebsockify written in glib/C  faster than then original (pytthon)  websockify
>Confidential:   no
>Severity:       serious
>Priority:       medium
>Responsible:    pkg-manager
>State:          open
>Class:          sw-bug
>Submitter-Id:   net
>Arrival-Date:   Thu Sep 10 17:35:01 +0000 2026
>Originator:     sergio lenzi
>Release:        
>Organization:
k1 sistemas
>Environment:
>Description:
# This is a shell archive.  Save it in a file, remove anything before
# this line, and then unpack it by entering "sh file".  Note, it may
# create directories; files and directories will be owned by you and
# have default permissions.
#
# This archive contains:
#
#	cwebsockify
#	cwebsockify/PLIST
#	cwebsockify/Makefile
#	cwebsockify/DESCR
#	cwebsockify/files
#	cwebsockify/files/Makefile
#	cwebsockify/files/ws.c
#	cwebsockify/files/tls.h
#	cwebsockify/files/conn.c
#	cwebsockify/files/http.c
#	cwebsockify/files/websockify.c
#	cwebsockify/files/token.c
#	cwebsockify/files/ws.h
#	cwebsockify/files/cwebsockify.sh
#	cwebsockify/files/tls.c
#	cwebsockify/files/websockify.1
#	cwebsockify/files/token.h
#	cwebsockify/files/http.h
#	cwebsockify/files/conn.h
#	cwebsockify
#
echo c - cwebsockify
mkdir -p cwebsockify > /dev/null 2>&1
echo x - cwebsockify/PLIST
sed 's/^X//' >cwebsockify/PLIST << 'END-of-cwebsockify/PLIST'
X@comment $NetBSD$
Xbin/websockify
Xman/man1/websockify.1
END-of-cwebsockify/PLIST
echo x - cwebsockify/Makefile
sed 's/^X//' >cwebsockify/Makefile << 'END-of-cwebsockify/Makefile'
X# $NetBSD$
X#
X# A C implementation of websockify, replacing www/websockify.  Same
X# program name and the same command line for the options that are
X# actually used, including TLS, so rc.d scripts and novnc-launch(1) need
X# no changes.
X#
X# Sources live in files/ and are copied into WRKSRC by do-extract; the
X# build itself is plain bsd.prog.mk.
X
XDISTNAME=	cwebsockify-1.0
XPKGNAME=	cwebsockify-1.0
XCATEGORIES=	mate www
XMASTER_SITES=	# local package
XDISTFILES=	# local package
X
XMAINTAINER=	rc_lenzi%yahoo.com.br@localhost
XHOMEPAGE=	https://github.com/novnc/websockify
XCOMMENT=	WebSocket to TCP proxy/bridge, in C
XLICENSE=	gnu-lgpl-v3
X
XCONFLICTS+=	websockify-[0-7]*
X
XUSE_TOOLS+=	pax pkg-config
XNO_CONFIGURE=	yes
XWRKSRC=		${WRKDIR}/${DISTNAME}
X
X# TLS is GIO's.  The backend is a module GIO opens at run time, so it is
X# a DEPENDS rather than a buildlink: nothing in it is linked against.
XDEPENDS+=	glib-networking>=2.60:../../net/glib-networking
X
X# Named cwebsockify, not websockify: mate/noVNC already installs an rc.d
X# script called websockify for its own service, and two installed packages
X# cannot own share/examples/rc.d/websockify between them.  pkgsrc takes the
X# source from files/cwebsockify.sh, substitutes @PREFIX@ and @VARBASE@, and
X# adds the result to the PLIST itself.
XRCD_SCRIPTS=	cwebsockify
X
XINSTALLATION_DIRS+=	bin
XINSTALLATION_DIRS+=	${PKGMANDIR}/man1
X
X# The install is bsd.prog.mk's own: pkgsrc runs its install target with
X# DESTDIR already set, so only the destination paths and ownership have
X# to be pointed at ${PREFIX} instead of the base system.
XMAKE_FLAGS+=	BINDIR=${PREFIX}/bin
XMAKE_FLAGS+=	MANDIR=${PREFIX}/${PKGMANDIR}
XMAKE_FLAGS+=	BINOWN=${BINOWN} BINGRP=${BINGRP}
XMAKE_FLAGS+=	MANOWN=${MANOWN} MANGRP=${MANGRP}
X
Xdo-extract:
X	${MKDIR} ${WRKSRC}
X	cd ${FILESDIR} && ${PAX} -rw . ${WRKSRC}
X
Xpre-build:
X	cd ${WRKSRC} && ${MAKE} depend
X
X
X.include "../../devel/glib2/buildlink3.mk"
X.include "../../mk/bsd.pkg.mk"
END-of-cwebsockify/Makefile
echo x - cwebsockify/DESCR
sed 's/^X//' >cwebsockify/DESCR << 'END-of-cwebsockify/DESCR'
Xwebsockify accepts WebSocket connections and relays them to a plain TCP
Xservice, which is how browser VNC clients such as noVNC reach a VNC
Xserver: the browser cannot open a raw socket, so the proxy unwraps the
XWebSocket framing and copies the bytes on.
X
XThis is a C implementation of the tool from the noVNC project, whose
Xreference version is written in Python.  It covers the options that
Xmatter in practice -- static file serving for the noVNC client, token
Xbased routing so one listener can serve many sessions, TLS with
X--cert/--key/--ssl-only, and daemonisation -- with no interpreter.  It is
Xbuilt on GLib: a GMainLoop owns the accept path and the per-request
Xdeadlines, and a GThreadPool does the blocking work.
X
XWhether a connection is TLS is decided by its first byte, as in the
XPython implementation, and the certificate is re-read when it is
Xrenewed.
X
XIt installs the same bin/websockify and replaces www/websockify.
END-of-cwebsockify/DESCR
echo c - cwebsockify/files
mkdir -p cwebsockify/files > /dev/null 2>&1
echo x - cwebsockify/files/Makefile
sed 's/^X//' >cwebsockify/files/Makefile << 'END-of-cwebsockify/files/Makefile'
X# Makefile -- websockify (GLib implementation)
X#
X# Plain bsd.prog.mk: it compiles, and its own install target places the
X# program and the man page.  pkgsrc drives that target with DESTDIR and
X# with BINDIR/MANDIR pointed at ${PREFIX}.
X
XPROG=	websockify
X
XSRCS=	websockify.c conn.c http.c ws.c token.c tls.c
X
XMAN=	websockify.1
X
X# Standalone builds land in /usr/pkg; pkgsrc overrides both.
XPREFIX?=	/usr/pkg
XBINDIR?=	${PREFIX}/bin
XMANDIR?=	${PREFIX}/man
X
XMKHTML=		no
XMKDEBUG=	no
X
XGLIB_CFLAGS!=	pkg-config --cflags gio-2.0 gio-unix-2.0 glib-2.0 gthread-2.0
XGLIB_LIBS!=	pkg-config --libs   gio-2.0 gio-unix-2.0 glib-2.0 gthread-2.0
X
XCPPFLAGS+=	-I${.CURDIR}
XCFLAGS+=	${GLIB_CFLAGS}
XLDADD+=		${GLIB_LIBS}
X
X# GLib's headers do not survive the base system's stricter warnings.
XWARNS=	3
XCFLAGS+=	-Wno-cast-qual
X
X.include <bsd.prog.mk>
END-of-cwebsockify/files/Makefile
echo x - cwebsockify/files/ws.c
sed 's/^X//' >cwebsockify/files/ws.c << 'END-of-cwebsockify/files/ws.c'
X// SPDX-License-Identifier: LGPL-3.0-or-later
X// Copyright (c) 2026 Ricardo Lenzi
X
X#include <gio/gio.h>
X#include <glib.h>
X#include <string.h>
X
X#include "ws.h"
X
X/* The magic string every RFC 6455 server appends to the client key. */
X#define WS_GUID		"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
X
X#define WS_FIN		0x80
X#define WS_OPCODE_MASK	0x0f
X#define WS_MASK_BIT	0x80
X#define WS_PAYLOAD_MASK	0x7f
X#define WS_LEN_16BIT	126
X#define WS_LEN_64BIT	127
X#define WS_MASK_LEN	4
X
X/* A single message larger than this is refused rather than allocated. */
X#define WS_MSG_MAX	(16 * 1024 * 1024)
X
Xstatic gboolean	 ws_read_full(GInputStream *in, void *buf, gsize len,
X		    GCancellable *cancel, GError **error);
Xstatic gchar	*ws_accept_key(const gchar *key);
X
Xstatic gboolean
Xws_read_full(GInputStream *in, void *buf, gsize len, GCancellable *cancel,
X    GError **error)
X{
X	gsize got = 0;
X
X	/*
X	 * read_all() would treat a short read at EOF as success, so the
X	 * caller could not tell a truncated frame from a complete one.
X	 */
X	if (!g_input_stream_read_all(in, buf, len, &got, cancel, error)) {
X		return (FALSE);
X	}
X	if (got != len) {
X		g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_PARTIAL_INPUT,
X		    "connection closed mid-frame");
X		return (FALSE);
X	}
X	return (TRUE);
X}
X
X/*
X * Sec-WebSocket-Accept is base64(SHA1(key + GUID)).  SHA1 is a fixed
X * part of the handshake construction here, not a security choice.
X */
Xstatic gchar *
Xws_accept_key(const gchar *key)
X{
X	guint8 digest[20];
X	gsize len = sizeof(digest);
X	GChecksum *sum;
X	gchar *concat, *out;
X
X	concat = g_strconcat(key, WS_GUID, NULL);
X	sum = g_checksum_new(G_CHECKSUM_SHA1);
X	g_checksum_update(sum, (const guchar *)concat, (gssize)strlen(concat));
X	g_checksum_get_digest(sum, digest, &len);
X	out = g_base64_encode(digest, len);
X
X	g_checksum_free(sum);
X	g_free(concat);
X	return (out);
X}
X
Xgboolean
Xws_accept(GOutputStream *out, const gchar *key, const gchar *proto,
X    GCancellable *cancel, GError **error)
X{
X	gchar *accept, *resp;
X	gboolean ok;
X
X	accept = ws_accept_key(key);
X	resp = g_strdup_printf(
X	    "HTTP/1.1 101 Switching Protocols\r\n"
X	    "Upgrade: websocket\r\n"
X	    "Connection: Upgrade\r\n"
X	    "Sec-WebSocket-Accept: %s\r\n"
X	    "%s%s%s"
X	    "\r\n",
X	    accept,
X	    proto != NULL ? "Sec-WebSocket-Protocol: " : "",
X	    proto != NULL ? proto : "",
X	    proto != NULL ? "\r\n" : "");
X
X	ok = g_output_stream_write_all(out, resp, strlen(resp), NULL,
X	    cancel, error);
X
X	g_free(resp);
X	g_free(accept);
X	return (ok);
X}
X
Xgboolean
Xws_read_message(GInputStream *in, ws_msg_t *msg, GOutputStream *out,
X    GCancellable *cancel, GError **error)
X{
X	guint8 hdr[2], ext[8], mask[WS_MASK_LEN];
X	gboolean first = TRUE;
X
X	if (msg->payload == NULL) {
X		msg->payload = g_byte_array_new();
X	}
X	g_byte_array_set_size(msg->payload, 0);
X	msg->closed = FALSE;
X
X	for (;;) {
X		guint64 paylen;
X		gboolean fin, masked;
X		ws_opcode_t opcode;
X		guint offset, i;
X
X		if (!ws_read_full(in, hdr, sizeof(hdr), cancel, error)) {
X			return (FALSE);
X		}
X		fin = (hdr[0] & WS_FIN) != 0;
X		opcode = (ws_opcode_t)(hdr[0] & WS_OPCODE_MASK);
X		masked = (hdr[1] & WS_MASK_BIT) != 0;
X		paylen = hdr[1] & WS_PAYLOAD_MASK;
X
X		if (paylen == WS_LEN_16BIT) {
X			if (!ws_read_full(in, ext, 2, cancel, error)) {
X				return (FALSE);
X			}
X			paylen = ((guint64)ext[0] << 8) | ext[1];
X		} else if (paylen == WS_LEN_64BIT) {
X			if (!ws_read_full(in, ext, 8, cancel, error)) {
X				return (FALSE);
X			}
X			paylen = 0;
X			for (i = 0; i < 8; i++) {
X				paylen = (paylen << 8) | ext[i];
X			}
X		}
X
X		/* RFC 6455: every frame from a client must be masked. */
X		if (!masked) {
X			g_set_error_literal(error, G_IO_ERROR,
X			    G_IO_ERROR_INVALID_DATA, "unmasked client frame");
X			return (FALSE);
X		}
X		if (!ws_read_full(in, mask, sizeof(mask), cancel, error)) {
X			return (FALSE);
X		}
X
X		if (opcode == WS_OP_CLOSE) {
X			msg->closed = TRUE;
X			return (TRUE);
X		}
X		if (msg->payload->len + paylen > WS_MSG_MAX) {
X			g_set_error(error, G_IO_ERROR, G_IO_ERROR_NO_SPACE,
X			    "message over %d bytes refused", WS_MSG_MAX);
X			return (FALSE);
X		}
X
X		offset = msg->payload->len;
X		g_byte_array_set_size(msg->payload, offset + (guint)paylen);
X		if (paylen > 0 && !ws_read_full(in, msg->payload->data + offset,
X		    (gsize)paylen, cancel, error)) {
X			return (FALSE);
X		}
X		for (i = 0; i < (guint)paylen; i++) {
X			msg->payload->data[offset + i] ^= mask[i % WS_MASK_LEN];
X		}
X
X		if (opcode == WS_OP_PING) {
X			gboolean ok = ws_send_frame(out, WS_OP_PONG,
X			    msg->payload->data + offset, (gsize)paylen,
X			    cancel, error);
X
X			g_byte_array_set_size(msg->payload, offset);
X			if (!ok) {
X				return (FALSE);
X			}
X			continue;
X		}
X		if (opcode == WS_OP_PONG) {
X			g_byte_array_set_size(msg->payload, offset);
X			continue;
X		}
X
X		if (first) {
X			msg->opcode = opcode;
X			first = FALSE;
X		}
X		if (fin) {
X			return (TRUE);
X		}
X	}
X}
X
Xgboolean
Xws_send_frame(GOutputStream *out, ws_opcode_t opcode, const guint8 *payload,
X    gsize len, GCancellable *cancel, GError **error)
X{
X	guint8 hdr[10];
X	gsize hlen = 0;
X
X	hdr[hlen++] = (guint8)(WS_FIN | opcode);
X
X	/* Frames from the server are never masked. */
X	if (len < WS_LEN_16BIT) {
X		hdr[hlen++] = (guint8)len;
X	} else if (len <= G_MAXUINT16) {
X		hdr[hlen++] = WS_LEN_16BIT;
X		hdr[hlen++] = (guint8)((len >> 8) & 0xff);
X		hdr[hlen++] = (guint8)(len & 0xff);
X	} else {
X		gint i;
X
X		hdr[hlen++] = WS_LEN_64BIT;
X		for (i = 7; i >= 0; i--) {
X			hdr[hlen++] = (guint8)((len >> (i * 8)) & 0xff);
X		}
X	}
X
X	if (!g_output_stream_write_all(out, hdr, hlen, NULL, cancel, error)) {
X		return (FALSE);
X	}
X	if (len > 0 && !g_output_stream_write_all(out, payload, len, NULL,
X	    cancel, error)) {
X		return (FALSE);
X	}
X	return (TRUE);
X}
X
Xvoid
Xws_msg_clear(ws_msg_t *msg)
X{
X	g_clear_pointer(&msg->payload, g_byte_array_unref);
X}
END-of-cwebsockify/files/ws.c
echo x - cwebsockify/files/tls.h
sed 's/^X//' >cwebsockify/files/tls.h << 'END-of-cwebsockify/files/tls.h'
X// SPDX-License-Identifier: LGPL-3.0-or-later
X// Copyright (c) 2026 Ricardo Lenzi
X
X#ifndef WEBSOCKIFY_TLS_H
X#define WEBSOCKIFY_TLS_H
X
X#include <gio/gio.h>
X#include <glib.h>
X
X/* First byte of a TLS record, and of an SSLv2 hello. */
X#define TLS_BYTE_HANDSHAKE	0x16
X#define TLS_BYTE_SSLV2		0x80
X
X/*
X * Load the certificate and the client CA database.  Called once, before
X * the daemon forks, so a bad certificate is reported to the terminal
X * rather than to a log nobody is watching.  cert == NULL disables TLS.
X */
Xgboolean	 tls_init(const gchar *cert, const gchar *key,
X		    gboolean verify_client, const gchar *cafile,
X		    GError **error);
Xvoid		 tls_shutdown(void);
X
Xgboolean	 tls_enabled(void);
X
X/*
X * Wrap an accepted connection and complete the handshake.  Returns a new
X * GIOStream owned by the caller, or NULL.  Called from worker threads.
X */
XGIOStream	*tls_wrap(GIOStream *base, GCancellable *cancel,
X		    GError **error);
X
X#endif /* WEBSOCKIFY_TLS_H */
END-of-cwebsockify/files/tls.h
echo x - cwebsockify/files/conn.c
sed 's/^X//' >cwebsockify/files/conn.c << 'END-of-cwebsockify/files/conn.c'
X// SPDX-License-Identifier: LGPL-3.0-or-later
X// Copyright (c) 2026 Ricardo Lenzi
X
X#include <gio/gio.h>
X#include <glib.h>
X#include <string.h>
X
X#include "conn.h"
X#include "http.h"
X#include "tls.h"
X#include "token.h"
X#include "ws.h"
X
X#define RELAY_BUF	65536
X
X/*
X * Relay state.  Both directions are watched through the connection's own
X * GMainContext, so the worker blocks in poll() inside g_main_loop_run()
X * rather than spinning.
X */
Xtypedef struct {
X	conn_t		*conn;
X	GMainLoop	*loop;
X	GInputStream	*ws_in;
X	GOutputStream	*ws_out;
X	GInputStream	*tg_in;
X	GOutputStream	*tg_out;
X	ws_msg_t	 msg;
X} relay_t;
X
Xstatic gboolean	 conn_deadline_cb(gpointer data);
Xstatic void	 conn_unref_notify(gpointer data);
Xstatic gboolean	 conn_peek_first(conn_t *conn, guint8 *first, GError **error);
Xstatic gboolean	 conn_start_stream(conn_t *conn);
Xstatic GSocketConnection *conn_open_target(conn_t *conn,
X		    const http_req_t *req, GError **error);
Xstatic GSource	*relay_source_new(GInputStream *in, GCancellable *cancel);
Xstatic gboolean	 relay_from_client(GObject *stream, gpointer data);
Xstatic gboolean	 relay_from_target(GObject *stream, gpointer data);
Xstatic void	 conn_relay(conn_t *conn, GSocketConnection *target);
X
Xconn_t *
Xconn_new(GSocketConnection *client, const conn_config_t *cfg)
X{
X	conn_t *conn = g_new0(conn_t, 1);
X
X	conn->client = g_object_ref(client);
X	conn->cancel = g_cancellable_new();
X	conn->cfg = cfg;
X	g_mutex_init(&conn->lock);
X	g_ref_count_init(&conn->refs);		/* the caller's reference */
X
X	/*
X	 * The deadline lives in the main loop, not in the worker: a worker
X	 * blocked on a client that never finishes its request cannot time
X	 * itself out.  Cancelling the GCancellable from here unblocks it.
X	 *
X	 * The source takes a reference of its own, given back by the
X	 * GDestroyNotify -- which GLib runs only once a dispatch already
X	 * in flight has returned.  That is what lets the worker drop its
X	 * reference at any moment: the two threads meet here, and the
X	 * mutex alone orders the fields, not the lifetime.
X	 */
X	g_ref_count_inc(&conn->refs);
X	conn->timer_id = g_timeout_add_seconds_full(G_PRIORITY_DEFAULT,
X	    cfg->timeout, conn_deadline_cb, conn, conn_unref_notify);
X
X	return (conn);
X}
X
Xvoid
Xconn_unref(conn_t *conn)
X{
X	if (conn == NULL || !g_ref_count_dec(&conn->refs)) {
X		return;
X	}
X	if (conn->stream != NULL &&
X	    conn->stream != G_IO_STREAM(conn->client)) {
X		g_object_unref(conn->stream);
X	}
X	conn->stream = NULL;
X	g_clear_object(&conn->client);
X	g_clear_object(&conn->cancel);
X	g_mutex_clear(&conn->lock);
X	g_free(conn);
X}
X
X/* GDestroyNotify for the timer's reference; may run in either thread. */
Xstatic void
Xconn_unref_notify(gpointer data)
X{
X	conn_unref(data);
X}
X
X/* Runs in the main loop. */
Xstatic gboolean
Xconn_deadline_cb(gpointer data)
X{
X	conn_t *conn = data;
X	gboolean settled;
X
X	g_mutex_lock(&conn->lock);
X	settled = conn->settled;
X	conn->timer_id = 0;
X	g_mutex_unlock(&conn->lock);
X
X	if (!settled) {
X		g_debug("request timed out before it was complete");
X		g_cancellable_cancel(conn->cancel);
X	}
X	return (G_SOURCE_REMOVE);
X}
X
X/*
X * The request is dealt with: either it is being relayed, which may last
X * hours, or it has been answered.  Either way the deadline must stop
X * applying.
X */
Xvoid
Xconn_settle(conn_t *conn)
X{
X	guint id;
X
X	g_mutex_lock(&conn->lock);
X	conn->settled = TRUE;
X	id = conn->timer_id;
X	conn->timer_id = 0;
X	g_mutex_unlock(&conn->lock);
X
X	if (id != 0) {
X		g_source_remove(id);
X	}
X}
X
X/*
X * Look at the first byte without consuming it, so that the connection
X * can still be handed to the TLS layer whole.  The deadline applies:
X * cancelling makes this return.
X */
Xstatic gboolean
Xconn_peek_first(conn_t *conn, guint8 *first, GError **error)
X{
X	GSocket *sock = g_socket_connection_get_socket(conn->client);
X	GInputVector vec;
X	gint flags = G_SOCKET_MSG_PEEK;
X	gssize n;
X
X	vec.buffer = first;
X	vec.size = 1;
X
X	n = g_socket_receive_message(sock, NULL, &vec, 1, NULL, NULL, &flags,
X	    conn->cancel, error);
X	if (n < 0) {
X		return (FALSE);
X	}
X	if (n == 0) {
X		g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_PARTIAL_INPUT,
X		    "closed before the first byte");
X		return (FALSE);
X	}
X	return (TRUE);
X}
X
X/*
X * Decide between TLS and plain HTTP the way www/websockify decides it:
X * 0x16 is a TLS handshake record, 0x80 an SSLv2 hello, anything else is
X * a plain client -- which --ssl-only refuses.
X *
X * With no certificate configured there is nothing to decide, and the
X * peek is skipped so that the common case costs nothing.
X */
Xstatic gboolean
Xconn_start_stream(conn_t *conn)
X{
X	GError *err = NULL;
X	guint8 first = 0;
X
X	conn->stream = G_IO_STREAM(conn->client);
X
X	if (!tls_enabled()) {
X		return (TRUE);
X	}
X	if (!conn_peek_first(conn, &first, &err)) {
X		g_debug("nothing to read: %s",
X		    err != NULL ? err->message : "?");
X		g_clear_error(&err);
X		return (FALSE);
X	}
X
X	if (first != TLS_BYTE_HANDSHAKE && first != TLS_BYTE_SSLV2) {
X		if (conn->cfg->ssl_only) {
X			g_message("plain connection refused: --ssl-only");
X			return (FALSE);
X		}
X		return (TRUE);
X	}
X
X	conn->stream = tls_wrap(G_IO_STREAM(conn->client), conn->cancel, &err);
X	if (conn->stream == NULL) {
X		conn->stream = G_IO_STREAM(conn->client);
X		g_message("TLS handshake failed: %s",
X		    err != NULL ? err->message : "?");
X		g_clear_error(&err);
X		return (FALSE);
X	}
X	g_debug("TLS established");
X	return (TRUE);
X}
X
Xstatic GSocketConnection *
Xconn_open_target(conn_t *conn, const http_req_t *req, GError **error)
X{
X	GSocketConnection *target;
X	GSocketClient *client;
X	GSocketConnectable *addr;
X
X	client = g_socket_client_new();
X
X	/*
X	 * The target is a local VNC server, not a web resource: without
X	 * this GIO consults libproxy on every connection, and a system
X	 * proxy would silently divert the session.
X	 */
X	g_socket_client_set_enable_proxy(client, FALSE);
X
X	if (conn->cfg->unix_target != NULL) {
X		addr = G_SOCKET_CONNECTABLE(
X		    g_unix_socket_address_new(conn->cfg->unix_target));
X	} else if (conn->cfg->token_source != NULL) {
X		const gchar *tok = http_param(req, "token");
X		token_target_t *t;
X
X		if (tok == NULL) {
X			g_set_error_literal(error, G_IO_ERROR,
X			    G_IO_ERROR_INVALID_ARGUMENT, "no token in request");
X			g_object_unref(client);
X			return (NULL);
X		}
X		t = token_lookup(tok);
X		if (t == NULL) {
X			g_set_error_literal(error, G_IO_ERROR,
X			    G_IO_ERROR_NOT_FOUND, "unknown token");
X			g_object_unref(client);
X			return (NULL);
X		}
X		addr = G_SOCKET_CONNECTABLE(
X		    g_network_address_new(t->host, t->port));
X		token_target_free(t);
X	} else {
X		addr = G_SOCKET_CONNECTABLE(g_network_address_new(
X		    conn->cfg->target_host, conn->cfg->target_port));
X	}
X
X	target = g_socket_client_connect(client, addr, conn->cancel, error);
X
X	g_object_unref(addr);
X	g_object_unref(client);
X	return (target);
X}
X
X/*
X * Readiness has to be asked of the stream, not of the socket underneath
X * it: a TLS record already read and decrypted leaves the socket quiet
X * while a whole message waits in the connection's buffer, and a source
X * watching the file descriptor would sleep through it.  Both
X * GSocketInputStream and the TLS input stream are pollable and answer
X * for what they hold.
X */
Xstatic GSource *
Xrelay_source_new(GInputStream *in, GCancellable *cancel)
X{
X	GPollableInputStream *pollable;
X
X	if (!G_IS_POLLABLE_INPUT_STREAM(in)) {
X		return (NULL);
X	}
X	pollable = G_POLLABLE_INPUT_STREAM(in);
X	if (!g_pollable_input_stream_can_poll(pollable)) {
X		return (NULL);
X	}
X	return (g_pollable_input_stream_create_source(pollable, cancel));
X}
X
Xstatic gboolean
Xrelay_from_client(GObject *stream, gpointer data)
X{
X	relay_t *r = data;
X	GError *err = NULL;
X
X	if (!ws_read_message(r->ws_in, &r->msg, r->ws_out, r->conn->cancel,
X	    &err) || r->msg.closed) {
X		g_clear_error(&err);
X		g_main_loop_quit(r->loop);
X		return (G_SOURCE_REMOVE);
X	}
X	if (r->msg.payload->len > 0 &&
X	    !g_output_stream_write_all(r->tg_out, r->msg.payload->data,
X	    r->msg.payload->len, NULL, r->conn->cancel, &err)) {
X		g_clear_error(&err);
X		g_main_loop_quit(r->loop);
X		return (G_SOURCE_REMOVE);
X	}
X	return (G_SOURCE_CONTINUE);
X}
X
Xstatic gboolean
Xrelay_from_target(GObject *stream, gpointer data)
X{
X	relay_t *r = data;
X	guint8 buf[RELAY_BUF];
X	GError *err = NULL;
X	gssize n;
X
X	n = g_input_stream_read(r->tg_in, buf, sizeof(buf), r->conn->cancel,
X	    &err);
X	if (n <= 0) {
X		g_clear_error(&err);
X		g_main_loop_quit(r->loop);
X		return (G_SOURCE_REMOVE);
X	}
X	if (!ws_send_frame(r->ws_out, WS_OP_BIN, buf, (gsize)n,
X	    r->conn->cancel, &err)) {
X		g_clear_error(&err);
X		g_main_loop_quit(r->loop);
X		return (G_SOURCE_REMOVE);
X	}
X	return (G_SOURCE_CONTINUE);
X}
X
X/* Relay until either side closes. */
Xstatic void
Xconn_relay(conn_t *conn, GSocketConnection *target)
X{
X	GMainContext *ctx = g_main_context_new();
X	GSource *src_ws, *src_tg;
X	relay_t r;
X
X	memset(&r, 0, sizeof(r));
X	r.conn = conn;
X	r.loop = g_main_loop_new(ctx, FALSE);
X	r.ws_in = g_io_stream_get_input_stream(conn->stream);
X	r.ws_out = g_io_stream_get_output_stream(conn->stream);
X	r.tg_in = g_io_stream_get_input_stream(G_IO_STREAM(target));
X	r.tg_out = g_io_stream_get_output_stream(G_IO_STREAM(target));
X
X	g_main_context_push_thread_default(ctx);
X
X	src_ws = relay_source_new(r.ws_in, conn->cancel);
X	src_tg = relay_source_new(r.tg_in, conn->cancel);
X	if (src_ws == NULL || src_tg == NULL) {
X		g_warning("stream cannot be polled; connection dropped");
X		goto out;
X	}
X
X	g_source_set_callback(src_ws, G_SOURCE_FUNC(relay_from_client), &r,
X	    NULL);
X	g_source_attach(src_ws, ctx);
X	g_source_set_callback(src_tg, G_SOURCE_FUNC(relay_from_target), &r,
X	    NULL);
X	g_source_attach(src_tg, ctx);
X
X	g_main_loop_run(r.loop);
X
X	g_source_destroy(src_ws);
X	g_source_destroy(src_tg);
Xout:
X	if (src_ws != NULL) {
X		g_source_unref(src_ws);
X	}
X	if (src_tg != NULL) {
X		g_source_unref(src_tg);
X	}
X	g_main_context_pop_thread_default(ctx);
X
X	ws_msg_clear(&r.msg);
X	g_main_loop_unref(r.loop);
X	g_main_context_unref(ctx);
X}
X
Xvoid
Xconn_serve(gpointer data, gpointer user_data)
X{
X	conn_t *conn = data;
X	GSocketConnection *target = NULL;
X	GOutputStream *out;
X	GInputStream *in;
X	http_req_t req;
X	GError *err = NULL;
X	const gchar *key, *proto;
X
X	if (!conn_start_stream(conn)) {
X		goto done;
X	}
X
X	in = g_io_stream_get_input_stream(conn->stream);
X	out = g_io_stream_get_output_stream(conn->stream);
X
X	if (!http_read_request(in, &req, conn->cancel, &err)) {
X		g_debug("request not read: %s", err != NULL ? err->message : "?");
X		g_clear_error(&err);
X		goto done;
X	}
X
X	if (!req.is_upgrade) {
X		if (conn->cfg->web != NULL) {
X			(void)http_serve_file(out, conn->cfg->web, req.path,
X			    conn->cancel, NULL);
X		} else {
X			http_send_error(out, 405, "Method Not Allowed",
X			    conn->cancel);
X		}
X		goto done_req;
X	}
X
X	key = http_header(&req, "sec-websocket-key");
X	if (key == NULL) {
X		http_send_error(out, 400, "Bad Request", conn->cancel);
X		goto done_req;
X	}
X
X	target = conn_open_target(conn, &req, &err);
X	if (target == NULL) {
X		g_message("no target: %s", err != NULL ? err->message : "?");
X		g_clear_error(&err);
X		http_send_error(out, 502, "Bad Gateway", conn->cancel);
X		goto done_req;
X	}
X
X	proto = http_header(&req, "sec-websocket-protocol");
X	if (!ws_accept(out, key, proto, conn->cancel, &err)) {
X		g_clear_error(&err);
X		goto done_req;
X	}
X
X	/* Relaying may last hours; the request deadline stops here. */
X	conn_settle(conn);
X	g_debug("relaying %s", req.path);
X	conn_relay(conn, target);
X
Xdone_req:
X	http_req_clear(&req);
Xdone:
X	conn_settle(conn);
X	if (target != NULL) {
X		(void)g_io_stream_close(G_IO_STREAM(target), NULL, NULL);
X		g_object_unref(target);
X	}
X	if (conn->stream != NULL) {
X		(void)g_io_stream_close(conn->stream, NULL, NULL);
X	}
X	(void)g_io_stream_close(G_IO_STREAM(conn->client), NULL, NULL);
X	conn_unref(conn);
X}
END-of-cwebsockify/files/conn.c
echo x - cwebsockify/files/http.c
sed 's/^X//' >cwebsockify/files/http.c << 'END-of-cwebsockify/files/http.c'
X// SPDX-License-Identifier: LGPL-3.0-or-later
X// Copyright (c) 2026 Ricardo Lenzi
X
X#include <gio/gio.h>
X#include <glib.h>
X#include <glib/gstdio.h>
X#include <string.h>
X
X#include "http.h"
X
X#define HTTP_HEAD_MAX	8192
X#define HTTP_SEND_CHUNK	65536
X
Xstatic gboolean	 http_read_head(GInputStream *in, GString *head,
X		    GCancellable *cancel, GError **error);
Xstatic void	 http_parse_headers(http_req_t *req, gchar **lines);
Xstatic gchar	*http_resolve(const gchar *webroot, const gchar *path);
Xstatic const gchar *http_mime_type(const gchar *path);
X
X/*
X * Read up to the blank line ending the head, one byte at a time: for an
X * upgrade the frame stream follows immediately and must be left in the
X * socket for the caller.
X */
Xstatic gboolean
Xhttp_read_head(GInputStream *in, GString *head, GCancellable *cancel,
X    GError **error)
X{
X	gchar c;
X
X	while (head->len < HTTP_HEAD_MAX) {
X		gsize got = 0;
X
X		if (!g_input_stream_read_all(in, &c, 1, &got, cancel, error)) {
X			return (FALSE);
X		}
X		if (got != 1) {
X			g_set_error_literal(error, G_IO_ERROR,
X			    G_IO_ERROR_PARTIAL_INPUT, "closed during request");
X			return (FALSE);
X		}
X		g_string_append_c(head, c);
X		if (head->len >= 4 &&
X		    strcmp(head->str + head->len - 4, "\r\n\r\n") == 0) {
X			return (TRUE);
X		}
X	}
X	g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_MESSAGE_TOO_LARGE,
X	    "request head too large");
X	return (FALSE);
X}
X
Xstatic void
Xhttp_parse_headers(http_req_t *req, gchar **lines)
X{
X	guint i;
X
X	for (i = 1; lines[i] != NULL && *lines[i] != '\0'; i++) {
X		gchar *colon = strchr(lines[i], ':');
X		gchar *name, *value;
X
X		if (colon == NULL) {
X			continue;
X		}
X		*colon = '\0';
X		name = g_ascii_strdown(g_strstrip(lines[i]), -1);
X		value = g_strdup(g_strstrip(colon + 1));
X		g_hash_table_insert(req->headers, name, value);
X	}
X
X	/*
X	 * Only header names were lowercased, so the value has to be
X	 * matched case insensitively; it may also be a list.
X	 */
X	{
X		const gchar *up = http_header(req, "upgrade");
X
X		if (up != NULL) {
X			g_autofree gchar *low = g_ascii_strdown(up, -1);
X
X			req->is_upgrade = strstr(low, "websocket") != NULL;
X		}
X	}
X}
X
Xgboolean
Xhttp_read_request(GInputStream *in, http_req_t *req, GCancellable *cancel,
X    GError **error)
X{
X	GString *head = g_string_new(NULL);
X	gchar **lines, **parts;
X	gchar *q;
X	gboolean ok = FALSE;
X
X	memset(req, 0, sizeof(*req));
X	req->headers = g_hash_table_new_full(g_str_hash, g_str_equal,
X	    g_free, g_free);
X	req->query = g_hash_table_new_full(g_str_hash, g_str_equal,
X	    g_free, g_free);
X
X	if (!http_read_head(in, head, cancel, error)) {
X		g_string_free(head, TRUE);
X		return (FALSE);
X	}
X
X	lines = g_strsplit(head->str, "\r\n", -1);
X	parts = g_strsplit(lines[0] != NULL ? lines[0] : "", " ", 3);
X
X	if (parts[0] == NULL || parts[1] == NULL ||
X	    strcmp(parts[0], "GET") != 0) {
X		g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
X		    "only GET is supported");
X		goto out;
X	}
X
X	q = strchr(parts[1], '?');
X	if (q != NULL) {
X		gchar **pairs;
X		guint i;
X
X		*q = '\0';
X		pairs = g_strsplit(q + 1, "&", -1);
X		for (i = 0; pairs[i] != NULL; i++) {
X			gchar *eq = strchr(pairs[i], '=');
X
X			if (eq == NULL) {
X				continue;
X			}
X			*eq = '\0';
X			g_hash_table_insert(req->query, g_strdup(pairs[i]),
X			    g_uri_unescape_string(eq + 1, NULL));
X		}
X		g_strfreev(pairs);
X	}
X	req->path = g_uri_unescape_string(parts[1], NULL);
X	if (req->path == NULL) {
X		req->path = g_strdup(parts[1]);
X	}
X
X	http_parse_headers(req, lines);
X	ok = TRUE;
Xout:
X	g_strfreev(parts);
X	g_strfreev(lines);
X	g_string_free(head, TRUE);
X	return (ok);
X}
X
Xvoid
Xhttp_req_clear(http_req_t *req)
X{
X	g_clear_pointer(&req->path, g_free);
X	g_clear_pointer(&req->headers, g_hash_table_unref);
X	g_clear_pointer(&req->query, g_hash_table_unref);
X}
X
Xconst gchar *
Xhttp_header(const http_req_t *req, const gchar *name)
X{
X	return (g_hash_table_lookup(req->headers, name));
X}
X
Xconst gchar *
Xhttp_param(const http_req_t *req, const gchar *name)
X{
X	return (g_hash_table_lookup(req->query, name));
X}
X
Xstatic const gchar *
Xhttp_mime_type(const gchar *path)
X{
X	static const struct {
X		const gchar *ext;
X		const gchar *type;
X	} types[] = {
X		{ ".html",  "text/html; charset=utf-8" },
X		{ ".js",    "text/javascript; charset=utf-8" },
X		{ ".mjs",   "text/javascript; charset=utf-8" },
X		{ ".css",   "text/css; charset=utf-8" },
X		{ ".json",  "application/json" },
X		{ ".svg",   "image/svg+xml" },
X		{ ".png",   "image/png" },
X		{ ".ico",   "image/x-icon" },
X		{ ".woff2", "font/woff2" },
X	};
X	guint i;
X
X	for (i = 0; i < G_N_ELEMENTS(types); i++) {
X		if (g_str_has_suffix(path, types[i].ext)) {
X			return (types[i].type);
X		}
X	}
X	return ("application/octet-stream");
X}
X
X/*
X * Resolve path under webroot, refusing anything that escapes it.  Both
X * sides go through realpath so that "..", symlinks and any other
X * indirection are collapsed before the prefix is compared.
X */
Xstatic gchar *
Xhttp_resolve(const gchar *webroot, const gchar *path)
X{
X	gchar *root, *joined, *real = NULL;
X
X	if (strstr(path, "..") != NULL) {
X		return (NULL);
X	}
X	root = realpath(webroot, NULL);
X	if (root == NULL) {
X		return (NULL);
X	}
X	joined = g_build_filename(root,
X	    g_strcmp0(path, "/") == 0 ? "vnc.html" : path + (*path == '/'), NULL);
X	real = realpath(joined, NULL);
X	g_free(joined);
X
X	if (real != NULL && !g_str_has_prefix(real, root)) {
X		g_warning("refused path outside web root: %s", path);
X		g_clear_pointer(&real, free);
X	}
X	free(root);
X	return (real);
X}
X
Xvoid
Xhttp_send_error(GOutputStream *out, guint code, const gchar *reason,
X    GCancellable *cancel)
X{
X	gchar *resp = g_strdup_printf(
X	    "HTTP/1.1 %u %s\r\n"
X	    "Content-Type: text/plain; charset=utf-8\r\n"
X	    "Content-Length: %zu\r\n"
X	    "Connection: close\r\n"
X	    "\r\n%s\n",
X	    code, reason, strlen(reason) + 1, reason);
X
X	(void)g_output_stream_write_all(out, resp, strlen(resp), NULL,
X	    cancel, NULL);
X	g_free(resp);
X}
X
Xgboolean
Xhttp_serve_file(GOutputStream *out, const gchar *webroot, const gchar *path,
X    GCancellable *cancel, GError **error)
X{
X	gchar *real, *hdr;
X	GFileInputStream *fin;
X	GFile *file;
X	GFileInfo *info;
X	goffset size;
X	gboolean ok = FALSE;
X
X	real = http_resolve(webroot, path);
X	if (real == NULL) {
X		http_send_error(out, 404, "Not Found", cancel);
X		return (FALSE);
X	}
X
X	file = g_file_new_for_path(real);
X	info = g_file_query_info(file, G_FILE_ATTRIBUTE_STANDARD_SIZE ","
X	    G_FILE_ATTRIBUTE_STANDARD_TYPE, G_FILE_QUERY_INFO_NONE, cancel, NULL);
X	if (info == NULL ||
X	    g_file_info_get_file_type(info) != G_FILE_TYPE_REGULAR) {
X		http_send_error(out, 404, "Not Found", cancel);
X		goto out;
X	}
X	size = g_file_info_get_size(info);
X
X	fin = g_file_read(file, cancel, NULL);
X	if (fin == NULL) {
X		http_send_error(out, 403, "Forbidden", cancel);
X		goto out;
X	}
X
X	hdr = g_strdup_printf(
X	    "HTTP/1.1 200 OK\r\n"
X	    "Content-Type: %s\r\n"
X	    "Content-Length: %" G_GOFFSET_FORMAT "\r\n"
X	    "Connection: close\r\n"
X	    "\r\n",
X	    http_mime_type(real), size);
X	ok = g_output_stream_write_all(out, hdr, strlen(hdr), NULL, cancel, error);
X	g_free(hdr);
X
X	if (ok) {
X		ok = g_output_stream_splice(out, G_INPUT_STREAM(fin),
X		    G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE, cancel, error) >= 0;
X	}
X	g_object_unref(fin);
Xout:
X	g_clear_object(&info);
X	g_object_unref(file);
X	free(real);
X	return (ok);
X}
END-of-cwebsockify/files/http.c
echo x - cwebsockify/files/websockify.c
sed 's/^X//' >cwebsockify/files/websockify.c << 'END-of-cwebsockify/files/websockify.c'
X// SPDX-License-Identifier: LGPL-3.0-or-later
X// Copyright (c) 2026 Ricardo Lenzi
X//
X// websockify -- accept WebSocket connections and relay them to a TCP
X// service.  A GLib implementation of the tool from the noVNC project.
X//
X// A GMainLoop accepts connections and owns the per-request deadlines; a
X// GThreadPool does the blocking work, so one stalled client cannot hold
X// up the accept path.
X
X#include <gio/gio.h>
X#include <glib-unix.h>
X#include <glib.h>
X#include <glib/gstdio.h>
X#include <signal.h>
X#include <string.h>
X#include <unistd.h>
X
X#include "conn.h"
X#include "tls.h"
X#include "token.h"
X
X#define DEFAULT_TIMEOUT	30	/* seconds allowed to send the request head */
X#define POOL_THREADS	32	/* concurrent connections being served */
X
Xstatic gchar	*opt_web;
Xstatic gchar	*opt_token_plugin;
Xstatic gchar	*opt_token_source;
Xstatic gchar	*opt_unix_target;
Xstatic gchar	*opt_log_file;
Xstatic gchar	*opt_pidfile;
Xstatic gchar	*opt_cert;
Xstatic gchar	*opt_key;
Xstatic gchar	*opt_cafile;
Xstatic gboolean	 opt_ssl_only;
Xstatic gboolean	 opt_verify_client;
Xstatic gboolean	 opt_ssl_target;
Xstatic gchar	*opt_ssl_version;
Xstatic gchar	*opt_ssl_ciphers;
Xstatic gchar	*opt_key_password;
Xstatic gboolean	 opt_inetd;
Xstatic gchar	*opt_wrap_mode;
Xstatic gchar	*opt_record;
Xstatic gchar	*opt_auth_plugin;
Xstatic gboolean	 opt_daemon;
Xstatic gboolean	 opt_verbose;
Xstatic gint	 opt_timeout = DEFAULT_TIMEOUT;
X
Xstatic GOptionEntry entries[] = {
X	{ "web", 0, 0, G_OPTION_ARG_FILENAME, &opt_web,
X	  "Serve static files from DIR", "DIR" },
X	{ "token-plugin", 0, 0, G_OPTION_ARG_STRING, &opt_token_plugin,
X	  "TokenFile or ReadOnlyTokenFile", "CLASS" },
X	{ "token-source", 0, 0, G_OPTION_ARG_FILENAME, &opt_token_source,
X	  "Token file, or a directory of them", "PATH" },
X	{ "unix-target", 0, 0, G_OPTION_ARG_FILENAME, &opt_unix_target,
X	  "Relay to a unix socket instead of TCP", "FILE" },
X	{ "cert", 0, 0, G_OPTION_ARG_FILENAME, &opt_cert,
X	  "Certificate for TLS, PEM (with the key, unless --key)", "FILE" },
X	{ "key", 0, 0, G_OPTION_ARG_FILENAME, &opt_key,
X	  "Private key, if it is not in the certificate file", "FILE" },
X	{ "ssl-only", 0, 0, G_OPTION_ARG_NONE, &opt_ssl_only,
X	  "Refuse connections that are not TLS", NULL },
X	{ "verify-client", 0, 0, G_OPTION_ARG_NONE, &opt_verify_client,
X	  "Require a client certificate", NULL },
X	{ "cafile", 0, 0, G_OPTION_ARG_FILENAME, &opt_cafile,
X	  "Authorities trusted for --verify-client", "FILE" },
X	{ "timeout", 0, 0, G_OPTION_ARG_INT, &opt_timeout,
X	  "Seconds allowed to send the request head", "SECONDS" },
X	{ "log-file", 0, 0, G_OPTION_ARG_FILENAME, &opt_log_file,
X	  "Append messages to FILE", "FILE" },
X	{ "pidfile", 0, 0, G_OPTION_ARG_FILENAME, &opt_pidfile,
X	  "Write the process id to FILE, for rc.d", "FILE" },
X	{ "daemon", 'D', 0, G_OPTION_ARG_NONE, &opt_daemon,
X	  "Run in the background", NULL },
X	{ "verbose", 'v', 0, G_OPTION_ARG_NONE, &opt_verbose,
X	  "Log every connection", NULL },
X	/* Present so they can be refused by name rather than ignored. */
X	{ "ssl-target", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE,
X	  &opt_ssl_target, NULL, NULL },
X	{ "ssl-version", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING,
X	  &opt_ssl_version, NULL, NULL },
X	{ "ssl-ciphers", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING,
X	  &opt_ssl_ciphers, NULL, NULL },
X	{ "key-password", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING,
X	  &opt_key_password, NULL, NULL },
X	{ "inetd", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &opt_inetd,
X	  NULL, NULL },
X	{ "wrap-mode", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING,
X	  &opt_wrap_mode, NULL, NULL },
X	{ "record", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_FILENAME,
X	  &opt_record, NULL, NULL },
X	{ "auth-plugin", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING,
X	  &opt_auth_plugin, NULL, NULL },
X	{ NULL, 0, 0, 0, NULL, NULL, NULL }
X};
X
Xstatic void	 log_handler(const gchar *domain, GLogLevelFlags level,
X		    const gchar *message, gpointer data);
Xstatic gboolean	 parse_endpoint(const gchar *arg, gchar **host, guint16 *port);
Xstatic gchar	*absolute(gchar *path);
Xstatic void	 reject_unsupported(void);
Xstatic void	 check_tls_options(void);
Xstatic gboolean	 on_incoming(GSocketService *service,
X		    GSocketConnection *client, GObject *source, gpointer data);
Xstatic gboolean	 on_signal(gpointer data);
X
Xstatic FILE	*log_stream;
Xstatic FILE	*pid_stream;
X
Xstatic void
Xlog_handler(const gchar *domain, GLogLevelFlags level, const gchar *message,
X    gpointer data)
X{
X	FILE *out = log_stream != NULL ? log_stream : stderr;
X	g_autofree gchar *stamp = NULL;
X	GDateTime *now;
X
X	if ((level & G_LOG_LEVEL_DEBUG) != 0 && !opt_verbose) {
X		return;
X	}
X	now = g_date_time_new_now_local();
X	stamp = g_date_time_format(now, "%Y-%m-%d %H:%M:%S");
X	g_date_time_unref(now);
X
X	(void)fprintf(out, "%s %s: %s\n", stamp,
X	    (level & G_LOG_LEVEL_WARNING) != 0 ? "warn" :
X	    (level & G_LOG_LEVEL_DEBUG) != 0 ? "debug" : "info", message);
X	(void)fflush(out);
X}
X
X/* "port", "host:port" or "[v6addr]:port". */
Xstatic gboolean
Xparse_endpoint(const gchar *arg, gchar **host, guint16 *port)
X{
X	g_autofree gchar *buf = g_strdup(arg);
X	gchar *colon;
X
X	*host = NULL;
X
X	if (buf[0] == '[') {
X		gchar *close = strchr(buf, ']');
X
X		if (close == NULL || close[1] != ':') {
X			return (FALSE);
X		}
X		*close = '\0';
X		*host = g_strdup(buf + 1);
X		*port = (guint16)g_ascii_strtoull(close + 2, NULL, 10);
X		return (*port != 0);
X	}
X
X	colon = strrchr(buf, ':');
X	if (colon == NULL) {
X		*port = (guint16)g_ascii_strtoull(buf, NULL, 10);
X		return (*port != 0);
X	}
X	*colon = '\0';
X	*host = g_strdup(buf);
X	*port = (guint16)g_ascii_strtoull(colon + 1, NULL, 10);
X	return (*port != 0);
X}
X
X/*
X * daemon(3) leaves the process in "/", so every path given on the
X * command line is resolved against the working directory it was started
X * from, before the fork.  Frees the argument.
X */
Xstatic gchar *
Xabsolute(gchar *path)
X{
X	gchar *out;
X
X	if (path == NULL || g_path_is_absolute(path)) {
X		return (path);
X	}
X	out = g_canonicalize_filename(path, NULL);
X	g_free(path);
X	return (out);
X}
X
Xstatic void
Xreject_unsupported(void)
X{
X	if (opt_ssl_target) {
X		g_printerr("websockify: --ssl-target is not supported; the "
X		    "target is reached over loopback\n");
X		exit(EXIT_FAILURE);
X	}
X	if (opt_ssl_version != NULL || opt_ssl_ciphers != NULL) {
X		g_printerr("websockify: --ssl-version and --ssl-ciphers are "
X		    "not supported;\nthe GnuTLS backend negotiates its own "
X		    "defaults, which exclude everything\nbelow TLS 1.2\n");
X		exit(EXIT_FAILURE);
X	}
X	if (opt_key_password != NULL) {
X		g_printerr("websockify: --key-password is not supported; "
X		    "give --key an unencrypted key\n");
X		exit(EXIT_FAILURE);
X	}
X	if (opt_inetd) {
X		g_printerr("websockify: --inetd is not supported; run it as a "
X		    "daemon instead\n");
X		exit(EXIT_FAILURE);
X	}
X	if (opt_wrap_mode != NULL) {
X		g_printerr("websockify: --wrap-mode is not supported; start the "
X		    "target from rc.d and give its address here\n");
X		exit(EXIT_FAILURE);
X	}
X	if (opt_record != NULL) {
X		g_printerr("websockify: --record is not supported; capture the "
X		    "traffic with tcpdump\n");
X		exit(EXIT_FAILURE);
X	}
X	if (opt_auth_plugin != NULL) {
X		g_printerr("websockify: --auth-plugin is not supported; "
X		    "authenticate in the reverse proxy in front\n");
X		exit(EXIT_FAILURE);
X	}
X	if (opt_token_plugin != NULL &&
X	    g_strcmp0(opt_token_plugin, "TokenFile") != 0 &&
X	    g_strcmp0(opt_token_plugin, "ReadOnlyTokenFile") != 0) {
X		g_printerr("websockify: token plugin '%s' is not supported; "
X		    "only TokenFile and ReadOnlyTokenFile exist here\n",
X		    opt_token_plugin);
X		exit(EXIT_FAILURE);
X	}
X}
X
X/* Options that are implemented, but only mean something together. */
Xstatic void
Xcheck_tls_options(void)
X{
X	if (opt_ssl_only && opt_cert == NULL) {
X		g_printerr("websockify: --ssl-only needs --cert\n");
X		exit(EXIT_FAILURE);
X	}
X	if (opt_key != NULL && opt_cert == NULL) {
X		g_printerr("websockify: --key needs --cert\n");
X		exit(EXIT_FAILURE);
X	}
X	if ((opt_verify_client || opt_cafile != NULL) && opt_cert == NULL) {
X		g_printerr("websockify: --verify-client needs --cert\n");
X		exit(EXIT_FAILURE);
X	}
X}
X
X/*
X * rc.subr stops a service by signalling the pid it read from the
X * pidfile, so the daemon has to leave on SIGTERM rather than be killed:
X * that is what lets it take the pidfile away with it.
X */
Xstatic gboolean
Xon_signal(gpointer data)
X{
X	GMainLoop *loop = data;
X
X	g_message("signal received, shutting down");
X	g_main_loop_quit(loop);
X	return (G_SOURCE_REMOVE);
X}
X
Xstatic gboolean
Xon_incoming(GSocketService *service, GSocketConnection *client,
X    GObject *source, gpointer data)
X{
X	GThreadPool *pool = data;
X	conn_config_t *cfg = g_object_get_data(G_OBJECT(service), "config");
X	conn_t *conn = conn_new(client, cfg);
X	GError *err = NULL;
X
X	if (!g_thread_pool_push(pool, conn, &err)) {
X		g_warning("cannot dispatch connection: %s", err->message);
X		g_clear_error(&err);
X		conn_settle(conn);	/* or the timer fires with no worker */
X		conn_unref(conn);
X	}
X	return (TRUE);	/* handled */
X}
X
Xint
Xmain(int argc, char *argv[])
X{
X	GOptionContext *octx;
X	GSocketService *service;
X	GThreadPool *pool;
X	GMainLoop *loop;
X	conn_config_t cfg;
X	GError *err = NULL;
X	gchar *listen_host = NULL;
X	guint16 listen_port = 0;
X
X	octx = g_option_context_new("[source_addr:]source_port "
X	    "[target_addr:target_port]");
X	g_option_context_set_summary(octx,
X	    "Accept WebSocket connections and relay them to a TCP service.\n"
X	    "A target is required unless --token-source or --unix-target is given.");
X	g_option_context_add_main_entries(octx, entries, NULL);
X	if (!g_option_context_parse(octx, &argc, &argv, &err)) {
X		g_printerr("websockify: %s\n", err->message);
X		return (EXIT_FAILURE);
X	}
X	g_option_context_free(octx);
X
X	reject_unsupported();
X	check_tls_options();
X
X	if (argc < 2) {
X		g_printerr("websockify: a listen address is required\n");
X		return (EXIT_FAILURE);
X	}
X
X	opt_web = absolute(opt_web);
X	opt_token_source = absolute(opt_token_source);
X	opt_unix_target = absolute(opt_unix_target);
X	opt_log_file = absolute(opt_log_file);
X	opt_pidfile = absolute(opt_pidfile);
X	opt_cert = absolute(opt_cert);
X	opt_key = absolute(opt_key);
X	opt_cafile = absolute(opt_cafile);
X
X	memset(&cfg, 0, sizeof(cfg));
X	cfg.web = opt_web;
X	cfg.token_source = opt_token_source;
X	cfg.unix_target = opt_unix_target;
X	cfg.ssl_only = opt_ssl_only;
X	cfg.timeout = (guint)MAX(opt_timeout, 1);
X
X	if (!parse_endpoint(argv[1], &listen_host, &listen_port)) {
X		g_printerr("websockify: cannot parse listen address: %s\n",
X		    argv[1]);
X		return (EXIT_FAILURE);
X	}
X	if (argc >= 3) {
X		if (!parse_endpoint(argv[2], &cfg.target_host,
X		    &cfg.target_port) || cfg.target_host == NULL) {
X			g_printerr("websockify: cannot parse target: %s\n",
X			    argv[2]);
X			return (EXIT_FAILURE);
X		}
X	} else if (cfg.token_source == NULL && cfg.unix_target == NULL) {
X		g_printerr("websockify: a target is required unless "
X		    "--token-source or --unix-target is given\n");
X		return (EXIT_FAILURE);
X	}
X
X	if (opt_log_file != NULL) {
X		log_stream = g_fopen(opt_log_file, "a");
X		if (log_stream == NULL) {
X			g_printerr("websockify: cannot open %s\n", opt_log_file);
X			return (EXIT_FAILURE);
X		}
X		(void)setvbuf(log_stream, NULL, _IOLBF, 0);
X	}
X	g_log_set_default_handler(log_handler, NULL);
X
X	if (!tls_init(opt_cert, opt_key, opt_verify_client, opt_cafile, &err)) {
X		g_printerr("websockify: %s\n", err->message);
X		return (EXIT_FAILURE);
X	}
X
X	/*
X	 * Opened here so that a directory that cannot be written to is
X	 * reported before the fork; the pid itself is only known after it.
X	 */
X	if (opt_pidfile != NULL) {
X		pid_stream = g_fopen(opt_pidfile, "w");
X		if (pid_stream == NULL) {
X			g_printerr("websockify: cannot write %s\n",
X			    opt_pidfile);
X			return (EXIT_FAILURE);
X		}
X	}
X
X	/*
X	 * Fork before anything in GLib has been built.  Everything above
X	 * this point only reads files and parses arguments, so a bad
X	 * certificate or an unwritable log is still reported to the
X	 * terminal; everything below allocates GObjects and threads, and
X	 * a fork under them leaves the child with a thread pool whose
X	 * workers never run -- the daemon then accepts connections and
X	 * answers none of them.
X	 *
X	 * stdio is closed: --log-file is the only output a daemon has,
X	 * and a child still holding the terminal means a pipe from
X	 * `websockify -D` never sees end of file.
X	 */
X	if (opt_daemon && daemon(0, 0) != 0) {
X		g_printerr("websockify: cannot daemonise\n");
X		return (EXIT_FAILURE);
X	}
X
X	if (pid_stream != NULL) {
X		(void)fprintf(pid_stream, "%ld\n", (long)getpid());
X		(void)fclose(pid_stream);
X		pid_stream = NULL;
X	}
X
X	token_init(cfg.token_source);
X
X	service = g_socket_service_new();
X	if (listen_host != NULL) {
X		GSocketAddress *addr;
X		GInetAddress *inet = g_inet_address_new_from_string(listen_host);
X
X		if (inet == NULL) {
X			g_printerr("websockify: cannot parse address %s\n",
X			    listen_host);
X			return (EXIT_FAILURE);
X		}
X		addr = g_inet_socket_address_new(inet, listen_port);
X		if (!g_socket_listener_add_address(G_SOCKET_LISTENER(service),
X		    addr, G_SOCKET_TYPE_STREAM, G_SOCKET_PROTOCOL_TCP, NULL,
X		    NULL, &err)) {
X			g_printerr("websockify: cannot listen on %s:%u: %s\n",
X			    listen_host, listen_port, err->message);
X			return (EXIT_FAILURE);
X		}
X		g_object_unref(addr);
X		g_object_unref(inet);
X	} else if (!g_socket_listener_add_inet_port(G_SOCKET_LISTENER(service),
X	    listen_port, NULL, &err)) {
X		g_printerr("websockify: cannot listen on port %u: %s\n",
X		    listen_port, err->message);
X		return (EXIT_FAILURE);
X	}
X
X	pool = g_thread_pool_new(conn_serve, NULL, POOL_THREADS, FALSE, &err);
X	if (pool == NULL) {
X		g_printerr("websockify: cannot create thread pool: %s\n",
X		    err->message);
X		return (EXIT_FAILURE);
X	}
X
X	g_object_set_data(G_OBJECT(service), "config", &cfg);
X	g_signal_connect(service, "incoming", G_CALLBACK(on_incoming), pool);
X
X	g_socket_service_start(service);
X	g_message("listening on %s:%u%s%s%s",
X	    listen_host != NULL ? listen_host : "*", listen_port,
X	    tls_enabled() ? (opt_ssl_only ? ", TLS only" : ", TLS or plain")
X	    : "",
X	    cfg.token_source != NULL ? ", targets from " : "",
X	    cfg.token_source != NULL ? cfg.token_source : "");
X
X	loop = g_main_loop_new(NULL, FALSE);
X	(void)g_unix_signal_add(SIGTERM, on_signal, loop);
X	(void)g_unix_signal_add(SIGINT, on_signal, loop);
X	g_main_loop_run(loop);
X
X	/*
X	 * Do not wait for the workers: each relayed session owns one for
X	 * as long as it lasts, and a stop must not hang behind an idle
X	 * VNC desktop.  Their sockets go when the process does.
X	 */
X	g_main_loop_unref(loop);
X	g_thread_pool_free(pool, TRUE, FALSE);
X	g_object_unref(service);
X	token_shutdown();
X	tls_shutdown();
X	if (opt_pidfile != NULL) {
X		(void)g_unlink(opt_pidfile);
X	}
X	g_free(listen_host);
X	g_free(cfg.target_host);
X
X	return (EXIT_SUCCESS);
X}
END-of-cwebsockify/files/websockify.c
echo x - cwebsockify/files/token.c
sed 's/^X//' >cwebsockify/files/token.c << 'END-of-cwebsockify/files/token.c'
X// SPDX-License-Identifier: LGPL-3.0-or-later
X// Copyright (c) 2026 Ricardo Lenzi
X
X#include <glib.h>
X#include <glib/gstdio.h>
X
X#include "token.h"
X
X/*
X * TokenFile: a file of "<token>: <host>:<port>" lines, or a directory of
X * such files.  The Python implementation re-reads the source on every
X * lookup, and gtkweb depends on that -- a session created after the
X * daemon started has to be reachable without a restart.
X *
X * Re-reading a directory per connection is cheap but not free, so the
X * parse is cached and invalidated by mtime.  Lookups happen on worker
X * threads, hence the mutex around both the cache and the stat.
X */
X
Xstatic GMutex	 token_lock;
Xstatic gchar	*token_source;
Xstatic GHashTable *token_cache;		/* token -> "host:port" */
Xstatic gint64	 token_mtime;
X
Xstatic void	 token_load_locked(void);
Xstatic void	 token_load_file(const gchar *path);
Xstatic gint64	 token_source_mtime(void);
X
Xvoid
Xtoken_init(const gchar *source)
X{
X	g_mutex_init(&token_lock);
X	token_source = g_strdup(source);
X	token_mtime = -1;
X}
X
Xvoid
Xtoken_shutdown(void)
X{
X	g_mutex_lock(&token_lock);
X	g_clear_pointer(&token_cache, g_hash_table_unref);
X	g_clear_pointer(&token_source, g_free);
X	g_mutex_unlock(&token_lock);
X	g_mutex_clear(&token_lock);
X}
X
X/* Newest mtime across the source, so added files are noticed too. */
Xstatic gint64
Xtoken_source_mtime(void)
X{
X	GStatBuf st;
X	gint64 newest;
X
X	if (token_source == NULL || g_stat(token_source, &st) != 0) {
X		return (-1);
X	}
X	newest = st.st_mtime;
X
X	if (S_ISDIR(st.st_mode)) {
X		const gchar *name;
X		GDir *dir = g_dir_open(token_source, 0, NULL);
X
X		if (dir == NULL) {
X			return (-1);
X		}
X		while ((name = g_dir_read_name(dir)) != NULL) {
X			gchar *p = g_build_filename(token_source, name, NULL);
X
X			if (g_stat(p, &st) == 0 && st.st_mtime > newest) {
X				newest = st.st_mtime;
X			}
X			g_free(p);
X		}
X		g_dir_close(dir);
X	}
X	return (newest);
X}
X
Xstatic void
Xtoken_load_file(const gchar *path)
X{
X	gchar *content = NULL;
X	gchar **lines;
X	guint i;
X
X	if (!g_file_get_contents(path, &content, NULL, NULL)) {
X		return;
X	}
X	lines = g_strsplit(content, "\n", -1);
X	for (i = 0; lines[i] != NULL; i++) {
X		gchar *line = g_strstrip(lines[i]);
X		gchar *colon;
X
X		if (*line == '\0' || *line == '#') {
X			continue;
X		}
X		colon = strchr(line, ':');
X		if (colon == NULL) {
X			g_warning("ignoring malformed token line in %s", path);
X			continue;
X		}
X		*colon = '\0';
X		g_hash_table_insert(token_cache, g_strdup(g_strstrip(line)),
X		    g_strdup(g_strstrip(colon + 1)));
X	}
X	g_strfreev(lines);
X	g_free(content);
X}
X
X/* Caller holds token_lock. */
Xstatic void
Xtoken_load_locked(void)
X{
X	gint64 mtime = token_source_mtime();
X	GStatBuf st;
X
X	if (token_cache != NULL && mtime == token_mtime) {
X		return;
X	}
X	g_clear_pointer(&token_cache, g_hash_table_unref);
X	token_cache = g_hash_table_new_full(g_str_hash, g_str_equal,
X	    g_free, g_free);
X	token_mtime = mtime;
X
X	if (token_source == NULL || g_stat(token_source, &st) != 0) {
X		return;
X	}
X	if (S_ISDIR(st.st_mode)) {
X		const gchar *name;
X		GDir *dir = g_dir_open(token_source, 0, NULL);
X
X		if (dir == NULL) {
X			g_warning("cannot read token directory %s", token_source);
X			return;
X		}
X		while ((name = g_dir_read_name(dir)) != NULL) {
X			gchar *p = g_build_filename(token_source, name, NULL);
X
X			token_load_file(p);
X			g_free(p);
X		}
X		g_dir_close(dir);
X	} else {
X		token_load_file(token_source);
X	}
X	g_debug("token cache reloaded: %u entries",
X	    g_hash_table_size(token_cache));
X}
X
Xtoken_target_t *
Xtoken_lookup(const gchar *token)
X{
X	token_target_t *out = NULL;
X	const gchar *target;
X
X	if (token == NULL || *token == '\0') {
X		return (NULL);
X	}
X
X	g_mutex_lock(&token_lock);
X	token_load_locked();
X	target = g_hash_table_lookup(token_cache, token);
X	if (target != NULL) {
X		/* The host may contain colons (IPv6): split at the last. */
X		const gchar *colon = strrchr(target, ':');
X
X		if (colon != NULL && colon != target) {
X			out = g_new0(token_target_t, 1);
X			out->host = g_strndup(target, (gsize)(colon - target));
X			out->port = (guint16)g_ascii_strtoull(colon + 1, NULL, 10);
X			if (out->port == 0) {
X				g_clear_pointer(&out->host, g_free);
X				g_clear_pointer(&out, g_free);
X			}
X		}
X	}
X	g_mutex_unlock(&token_lock);
X
X	return (out);
X}
X
Xvoid
Xtoken_target_free(token_target_t *t)
X{
X	if (t != NULL) {
X		g_free(t->host);
X		g_free(t);
X	}
X}
END-of-cwebsockify/files/token.c
echo x - cwebsockify/files/ws.h
sed 's/^X//' >cwebsockify/files/ws.h << 'END-of-cwebsockify/files/ws.h'
X// SPDX-License-Identifier: LGPL-3.0-or-later
X// Copyright (c) 2026 Ricardo Lenzi
X
X#ifndef WEBSOCKIFY_WS_H
X#define WEBSOCKIFY_WS_H
X
X#include <gio/gio.h>
X#include <glib.h>
X
X/* RFC 6455 opcodes. */
Xtypedef enum {
X	WS_OP_CONT  = 0x0,
X	WS_OP_TEXT  = 0x1,
X	WS_OP_BIN   = 0x2,
X	WS_OP_CLOSE = 0x8,
X	WS_OP_PING  = 0x9,
X	WS_OP_PONG  = 0xa
X} ws_opcode_t;
X
Xtypedef struct {
X	GByteArray	*payload;
X	ws_opcode_t	 opcode;
X	gboolean	 closed;
X} ws_msg_t;
X
X/* Send the 101 response completing the opening handshake. */
Xgboolean	 ws_accept(GOutputStream *out, const gchar *key,
X		    const gchar *proto, GCancellable *cancel, GError **error);
X
X/* Read one whole message, following continuation frames. */
Xgboolean	 ws_read_message(GInputStream *in, ws_msg_t *msg,
X		    GOutputStream *out, GCancellable *cancel, GError **error);
Xvoid		 ws_msg_clear(ws_msg_t *msg);
X
Xgboolean	 ws_send_frame(GOutputStream *out, ws_opcode_t opcode,
X		    const guint8 *payload, gsize len, GCancellable *cancel,
X		    GError **error);
X
X#endif /* WEBSOCKIFY_WS_H */
END-of-cwebsockify/files/ws.h
echo x - cwebsockify/files/cwebsockify.sh
sed 's/^X//' >cwebsockify/files/cwebsockify.sh << 'END-of-cwebsockify/files/cwebsockify.sh'
X#!@RCD_SCRIPTS_SHELL@
X#
X# $NetBSD$
X#
X# PROVIDE: cwebsockify
X# REQUIRE: DAEMON
X# KEYWORD: shutdown
X#
X# WebSocket to TCP proxy -- the C implementation from mate/websockify.
X#
X# The service is deliberately not called "websockify": mate/noVNC installs
X# an rc.d script of that name for its own noVNC front end, and two
X# installed packages cannot own the same file.  Both may run at once on
X# different ports; this one is the general purpose listener.
X#
X# Put cwebsockify=YES in /etc/rc.conf, with any of:
X#
X#   cwebsockify_listen="127.0.0.1:6080"  address to listen on, or a bare port
X#   cwebsockify_target="localhost:5900"  where to relay to; not used when a
X#                                        token source is given
X#   cwebsockify_web=""                   serve the noVNC client from here,
X#                                        e.g. @PREFIX@/share/noVNC
X#   cwebsockify_token_source=""          token file, or a directory of them
X#   cwebsockify_cert=""                  PEM certificate for TLS; it carries
X#                                        the key too unless _key is set
X#   cwebsockify_key=""                   private key, when kept separately
X#   cwebsockify_ssl_only="NO"            refuse connections that are not TLS
X#   cwebsockify_verify_client="NO"       require a client certificate
X#   cwebsockify_cafile=""                authorities for _verify_client
X#   cwebsockify_timeout=""               seconds allowed for the request head
X#   cwebsockify_logfile="@VARBASE@/log/cwebsockify.log"
X#   cwebsockify_user=""                  run as this user instead of root
X#   cwebsockify_group=""                 and this group
X#   cwebsockify_flags=""                 anything else to pass through
X#
X# The daemon forks itself with -D and writes the pidfile, so start, stop
X# and status are rc.subr's own: stop signals the pid and the daemon takes
X# the pidfile away with it.
X#
X
X. /etc/rc.subr
X
Xname="cwebsockify"
Xrcvar=$name
Xcommand="@PREFIX@/bin/websockify"
X
X#
X# Its own directory under @VARBASE@/run.  The daemon both writes the
X# pidfile and unlinks it on the way out, and unlinking needs write
X# permission on the directory -- which an unprivileged cwebsockify_user
X# would not have on @VARBASE@/run itself.
X#
Xpidfile="@VARBASE@/run/${name}/${name}.pid"
X
Xstart_precmd="cwebsockify_precmd"
X
Xcwebsockify_precmd()
X{
X	_owner="${cwebsockify_user:-root}"
X	_group="${cwebsockify_group:-wheel}"
X
X	/usr/bin/install -d -o "${_owner}" -g "${_group}" -m 0755 \
X	    "@VARBASE@/run/${name}"
X
X	# Appended to by the daemon, so it has to exist and be writable by
X	# it; the directory above stays as it is.
X	if [ -n "${cwebsockify_logfile}" ] &&
X	   [ ! -f "${cwebsockify_logfile}" ]; then
X		/usr/bin/install -o "${_owner}" -g "${_group}" -m 0640 \
X		    /dev/null "${cwebsockify_logfile}"
X	fi
X}
X
Xload_rc_config $name
X
X: ${cwebsockify_listen:=127.0.0.1:6080}
X: ${cwebsockify_target:=localhost:5900}
X: ${cwebsockify_logfile:=@VARBASE@/log/${name}.log}
X: ${cwebsockify_ssl_only:=NO}
X: ${cwebsockify_verify_client:=NO}
X
X_args="-D --pidfile ${pidfile}"
X
Xif [ -n "${cwebsockify_logfile}" ]; then
X	_args="${_args} --log-file ${cwebsockify_logfile}"
Xfi
Xif [ -n "${cwebsockify_web}" ]; then
X	_args="${_args} --web ${cwebsockify_web}"
Xfi
Xif [ -n "${cwebsockify_timeout}" ]; then
X	_args="${_args} --timeout ${cwebsockify_timeout}"
Xfi
Xif [ -n "${cwebsockify_cert}" ]; then
X	_args="${_args} --cert ${cwebsockify_cert}"
X	if [ -n "${cwebsockify_key}" ]; then
X		_args="${_args} --key ${cwebsockify_key}"
X	fi
X	if checkyesno cwebsockify_ssl_only; then
X		_args="${_args} --ssl-only"
X	fi
X	if checkyesno cwebsockify_verify_client; then
X		_args="${_args} --verify-client"
X		if [ -n "${cwebsockify_cafile}" ]; then
X			_args="${_args} --cafile ${cwebsockify_cafile}"
X		fi
X	fi
Xfi
X
X#
X# A token source replaces the fixed target rather than adding to it: given
X# both, the token decides, and a request whose token is unknown would be
X# answered 502 instead of falling back.  Only one is ever passed.
X#
Xif [ -n "${cwebsockify_token_source}" ]; then
X	_args="${_args} --token-plugin TokenFile"
X	_args="${_args} --token-source ${cwebsockify_token_source}"
X	command_args="${_args} ${cwebsockify_listen}"
Xelse
X	command_args="${_args} ${cwebsockify_listen} ${cwebsockify_target}"
Xfi
X
Xrun_rc_command "$1"
END-of-cwebsockify/files/cwebsockify.sh
echo x - cwebsockify/files/tls.c
sed 's/^X//' >cwebsockify/files/tls.c << 'END-of-cwebsockify/files/tls.c'
X// SPDX-License-Identifier: LGPL-3.0-or-later
X// Copyright (c) 2026 Ricardo Lenzi
X
X#include <gio/gio.h>
X#include <glib.h>
X#include <glib/gstdio.h>
X
X#include "tls.h"
X
X/*
X * TLS on the listening side, with the same options as www/websockify:
X * --cert, --key, --ssl-only, --verify-client and --cafile.  Which of the
X * two a connection is speaking is decided by its first byte, exactly as
X * the Python implementation decides it.
X *
X * GIO does the protocol work.  That is not just for brevity: the earlier
X * OpenSSL revision of this program treated SSL_read() returning
X * WANT_READ or WANT_WRITE as fatal, so a TLS 1.3 key update in the
X * middle of a long VNC session dropped it.  Retrying on those belongs to
X * the TLS layer, and GTlsConnection does it.
X *
X * The certificate is reloaded when the file changes.  Loading it once at
X * startup was the other defect: after certbot renewed the certificate
X * the daemon went on presenting the expired one until it was restarted.
X */
X
Xstatic GMutex		 tls_lock;
Xstatic gchar		*tls_cert_path;
Xstatic gchar		*tls_key_path;
Xstatic gboolean		 tls_verify_client;
Xstatic GTlsCertificate	*tls_cert;	/* cached; reloaded on mtime change */
Xstatic GTlsDatabase	*tls_db;	/* client CAs, when verifying */
Xstatic gint64		 tls_mtime;
X
Xstatic const gchar	*tls_flags_str(GTlsCertificateFlags errors);
Xstatic gboolean		 tls_accept_cert(GTlsConnection *conn, GTlsCertificate *peer,
X			    GTlsCertificateFlags errors, gpointer data);
Xstatic gint64		 tls_files_mtime(void);
Xstatic GTlsCertificate	*tls_cert_ref_locked(GError **error);
X
Xgboolean
Xtls_enabled(void)
X{
X	return (tls_cert_path != NULL);
X}
X
X/* Newest mtime of the certificate and, if separate, the key. */
Xstatic gint64
Xtls_files_mtime(void)
X{
X	GStatBuf st;
X	gint64 newest = -1;
X
X	if (tls_cert_path != NULL && g_stat(tls_cert_path, &st) == 0) {
X		newest = st.st_mtime;
X	}
X	if (tls_key_path != NULL && g_stat(tls_key_path, &st) == 0 &&
X	    st.st_mtime > newest) {
X		newest = st.st_mtime;
X	}
X	return (newest);
X}
X
X/* Caller holds tls_lock. */
Xstatic GTlsCertificate *
Xtls_cert_ref_locked(GError **error)
X{
X	gint64 mtime = tls_files_mtime();
X
X	if (tls_cert != NULL && mtime == tls_mtime) {
X		return (g_object_ref(tls_cert));
X	}
X
X	g_clear_object(&tls_cert);
X	tls_mtime = mtime;
X
X	if (tls_key_path != NULL) {
X		tls_cert = g_tls_certificate_new_from_files(tls_cert_path,
X		    tls_key_path, error);
X	} else {
X		/* One PEM holding both, which is what --cert alone means. */
X		tls_cert = g_tls_certificate_new_from_file(tls_cert_path,
X		    error);
X	}
X	if (tls_cert == NULL) {
X		return (NULL);
X	}
X	g_message("certificate loaded from %s", tls_cert_path);
X	return (g_object_ref(tls_cert));
X}
X
Xgboolean
Xtls_init(const gchar *cert, const gchar *key, gboolean verify_client,
X    const gchar *cafile, GError **error)
X{
X	GTlsCertificate *loaded;
X
X	g_mutex_init(&tls_lock);
X	tls_mtime = -1;
X
X	if (cert == NULL) {
X		return (TRUE);
X	}
X	if (!g_tls_backend_supports_tls(g_tls_backend_get_default())) {
X		g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
X		    "no TLS backend: install net/glib-networking");
X		return (FALSE);
X	}
X
X	tls_cert_path = g_strdup(cert);
X	tls_key_path = g_strdup(key);
X	tls_verify_client = verify_client;
X
X	if (verify_client) {
X		if (cafile != NULL) {
X			tls_db = g_tls_file_database_new(cafile, error);
X		} else {
X			tls_db = g_tls_backend_get_default_database(
X			    g_tls_backend_get_default());
X		}
X		if (tls_db == NULL) {
X			return (FALSE);
X		}
X	}
X
X	/* Fail here rather than on the first connection. */
X	g_mutex_lock(&tls_lock);
X	loaded = tls_cert_ref_locked(error);
X	g_mutex_unlock(&tls_lock);
X
X	if (loaded == NULL) {
X		return (FALSE);
X	}
X	g_object_unref(loaded);
X	return (TRUE);
X}
X
Xvoid
Xtls_shutdown(void)
X{
X	g_mutex_lock(&tls_lock);
X	g_clear_object(&tls_cert);
X	g_clear_object(&tls_db);
X	g_clear_pointer(&tls_cert_path, g_free);
X	g_clear_pointer(&tls_key_path, g_free);
X	g_mutex_unlock(&tls_lock);
X	g_mutex_clear(&tls_lock);
X}
X
XGIOStream *
Xtls_wrap(GIOStream *base, GCancellable *cancel, GError **error)
X{
X	GTlsCertificate *cert;
X	GTlsDatabase *db = NULL;
X	GIOStream *tls;
X
X	g_mutex_lock(&tls_lock);
X	cert = tls_cert_ref_locked(error);
X	if (tls_db != NULL) {
X		db = g_object_ref(tls_db);
X	}
X	g_mutex_unlock(&tls_lock);
X
X	if (cert == NULL) {
X		return (NULL);
X	}
X
X	tls = g_tls_server_connection_new(base, cert, error);
X	g_object_unref(cert);
X	if (tls == NULL) {
X		g_clear_object(&db);
X		return (NULL);
X	}
X
X	/*
X	 * A browser tab that is closed goes away without a close_notify.
X	 * Requiring one turns every such disconnect into an error and can
X	 * block the close on a peer that is already gone.
X	 */
X	g_tls_connection_set_require_close_notify(G_TLS_CONNECTION(tls), FALSE);
X
X	if (tls_verify_client) {
X		g_object_set(tls, "authentication-mode",
X		    G_TLS_AUTHENTICATION_REQUIRED, NULL);
X		if (db != NULL) {
X			g_tls_connection_set_database(G_TLS_CONNECTION(tls),
X			    db);
X		}
X		g_signal_connect(tls, "accept-certificate",
X		    G_CALLBACK(tls_accept_cert), NULL);
X	}
X	g_clear_object(&db);
X
X	if (!g_tls_connection_handshake(G_TLS_CONNECTION(tls), cancel, error)) {
X		g_object_unref(tls);
X		return (NULL);
X	}
X	return (tls);
X}
X
X/* Why a client certificate was turned away, in the log. */
Xstatic const gchar *
Xtls_flags_str(GTlsCertificateFlags errors)
X{
X	static const struct {
X		GTlsCertificateFlags	 flag;
X		const gchar		*name;
X	} names[] = {
X		{ G_TLS_CERTIFICATE_UNKNOWN_CA,    "unknown CA" },
X		{ G_TLS_CERTIFICATE_BAD_IDENTITY,  "identity mismatch" },
X		{ G_TLS_CERTIFICATE_NOT_ACTIVATED, "not yet valid" },
X		{ G_TLS_CERTIFICATE_EXPIRED,       "expired" },
X		{ G_TLS_CERTIFICATE_REVOKED,       "revoked" },
X		{ G_TLS_CERTIFICATE_INSECURE,      "insecure algorithm" },
X		{ G_TLS_CERTIFICATE_GENERIC_ERROR, "generic error" }
X	};
X	static gchar buf[128];
X	guint i;
X
X	buf[0] = '\0';
X	for (i = 0; i < G_N_ELEMENTS(names); i++) {
X		if ((errors & names[i].flag) != 0) {
X			if (buf[0] != '\0') {
X				(void)g_strlcat(buf, ", ", sizeof(buf));
X			}
X			(void)g_strlcat(buf, names[i].name, sizeof(buf));
X		}
X	}
X	return (buf[0] != '\0' ? buf : "no error reported");
X}
X
X/*
X * Validate the client certificate.  A server connection reaches this
X * with errors == 0 whatever the certificate is: glib-networking hands
X * the peer certificate straight to the application without looking at
X * the database, because only the application knows what it will accept.
X * So the chain is checked here, against --cafile or the system store.
X */
Xstatic gboolean
Xtls_accept_cert(GTlsConnection *conn, GTlsCertificate *peer,
X    GTlsCertificateFlags errors, gpointer data)
X{
X	GTlsDatabase *db = g_tls_connection_get_database(conn);
X	GTlsCertificateFlags bad;
X	GError *err = NULL;
X
X	if (db == NULL) {
X		g_message("client certificate refused: no authorities loaded");
X		return (FALSE);
X	}
X
X	bad = g_tls_database_verify_chain(db, peer,
X	    G_TLS_DATABASE_PURPOSE_AUTHENTICATE_CLIENT, NULL, NULL,
X	    G_TLS_DATABASE_VERIFY_NONE, NULL, &err);
X	if (err != NULL) {
X		g_message("client certificate not checked: %s", err->message);
X		g_clear_error(&err);
X		return (FALSE);
X	}
X
X	bad |= errors;
X	if (bad != 0) {
X		g_message("client certificate refused: %s (0x%x)",
X		    tls_flags_str(bad), (guint)bad);
X		return (FALSE);
X	}
X	return (TRUE);
X}
END-of-cwebsockify/files/tls.c
echo x - cwebsockify/files/websockify.1
sed 's/^X//' >cwebsockify/files/websockify.1 << 'END-of-cwebsockify/files/websockify.1'
X.\" SPDX-License-Identifier: LGPL-3.0-or-later
X.\" Copyright (c) 2026 Ricardo Lenzi
X.Dd September 8, 2026
X.Dt WEBSOCKIFY 1
X.Os
X.Sh NAME
X.Nm websockify
X.Nd WebSocket to TCP proxy
X.Sh SYNOPSIS
X.Nm
X.Op Fl Dvh
X.Op Fl -web Ar dir
X.Op Fl -token-plugin Ar class
X.Op Fl -token-source Ar path
X.Op Fl -unix-target Ar file
X.Op Fl -cert Ar file
X.Op Fl -key Ar file
X.Op Fl -ssl-only
X.Op Fl -verify-client
X.Op Fl -cafile Ar file
X.Op Fl -timeout Ar seconds
X.Op Fl -log-file Ar file
X.Op Fl -pidfile Ar file
X.Oo Ar source_addr : Oc Ns Ar source_port
X.Op Ar target_addr : Ns Ar target_port
X.Sh DESCRIPTION
X.Nm
Xaccepts WebSocket connections and relays their payload to a plain TCP
Xservice, which is how a browser reaches a VNC server: the browser cannot
Xopen a raw socket, so the framing has to be removed by a proxy.
X.Pp
XThis is a C implementation of the tool from the noVNC project.
X.Pp
XThe options are:
X.Bl -tag -width Ds
X.It Fl -web Ar dir
XServe static files from
X.Ar dir
Xfor requests that are not WebSocket upgrades.
XPaths that resolve outside
X.Ar dir
Xare refused.
X.It Fl -token-plugin Ar class
XAccepts
X.Cm TokenFile
Xand
X.Cm ReadOnlyTokenFile .
XBoth behave the same here: the source is consulted on every connection.
X.It Fl -token-source Ar path
XA file of
X.Dq Ar token : Ar host : Ar port
Xlines, or a directory of such files.
XWhen given, the target comes from the
X.Va token
Xquery parameter instead of the command line, so one listener can serve
Xmany sessions.
XBecause the source is read per connection, sessions created after
X.Nm
Xstarted are reachable without a restart.
X.It Fl -unix-target Ar file
XRelay to a unix domain socket rather than to a TCP address.
X.It Fl -cert Ar file
XServe TLS using the certificate in
X.Ar file ,
Xwhich is PEM and holds the private key as well unless
X.Fl -key
Xis given.
XSee
X.Sx TLS .
X.It Fl -key Ar file
XThe private key, when it is kept apart from the certificate.
XThe key must not be encrypted.
X.It Fl -ssl-only
XRefuse connections that are not TLS.
XWithout it a listener with a certificate accepts both, deciding per
Xconnection.
X.It Fl -verify-client
XRequire the client to present a certificate, and refuse the connection
Xunless it chains to a trusted authority.
X.It Fl -cafile Ar file
XThe authorities trusted for
X.Fl -verify-client ,
Xas a PEM file.
XThe system trust store is used when this is not given.
X.It Fl -timeout Ar seconds
XHow long a client may take to send its request head before the
Xconnection is dropped; 30 by default.
XThe deadline is enforced by the main loop, so a client that connects and
Xthen stalls cannot pin a worker.
XIt covers the TLS handshake as well, and stops applying once relaying
Xstarts, since a session may sit idle for hours.
X.It Fl D , Fl -daemon
XDetach and run in the background with
X.Xr daemon 3 .
XThe fork happens after the arguments, the log file, the pidfile and the
Xcertificate have been checked, so a mistake in any of them is still
Xreported on the terminal; paths given on the command line are resolved
Xbefore it, so relative ones survive the change of working directory.
XStandard input, output and error are closed, which is why
X.Fl -log-file
Xis needed for anything to be recorded.
X.It Fl -log-file Ar file
XAppend messages to
X.Ar file
Xinstead of standard error.
XRequired for anything to be recorded after
X.Fl D .
X.It Fl -pidfile Ar file
XWrite the process id to
X.Ar file
Xonce the fork has happened, and remove it on exit.
X.Nm
Xleaves on
X.Dv SIGTERM
Xwithout waiting for sessions still being relayed, so a
X.Xr rc.subr 8
Xservice using this as its
X.Va pidfile
Xcan start, stop and report status.
XThe file must be somewhere the user the daemon runs as can write:
X.Pa /var/run
Xis not writable by an unprivileged one.
XThe Python implementation has no equivalent.
X.It Fl v , Fl -verbose
XLog every connection.
X.It Fl h , Fl -help
XPrint usage and exit.
X.El
X.Sh TLS
XA listener started with
X.Fl -cert
Xlooks at the first byte of each connection and treats it as TLS if that
Xbyte begins a handshake record, exactly as the Python implementation
Xdoes; anything else is served as plain HTTP unless
X.Fl -ssl-only
Xforbids it.
X.Pp
XThe certificate is re-read when the file changes, so a renewal by
Xcertbot takes effect on the next connection without a restart.
X.Pp
XTLS is provided by GIO, which needs
X.Pa net/glib-networking
Xinstalled; without it a certificate is refused at startup rather than at
Xthe first connection.
XProtocol versions and cipher suites are the backend's own and cannot be
Xselected, which is why
X.Fl -ssl-version
Xand
X.Fl -ssl-ciphers
Xare rejected.
X.Pp
XTerminating TLS here is for the case where
X.Nm
Xfaces the network directly.
XBehind a reverse proxy the usual arrangement is still better: let the
Xproxy hold the certificate and let
X.Nm
Xlisten on loopback without one.
XNote that a page served over HTTPS cannot open a
X.Ql ws://
XWebSocket, so a proxy that terminates TLS must be given a
X.Ql wss://
Xendpoint to forward to, or must reach
X.Nm
Xover loopback.
X.Sh FILES
X.Bl -tag -width Ds
X.It Pa /usr/pkg/share/examples/rc.d/cwebsockify
XAn
X.Xr rc.subr 8
Xscript for this proxy, copied to
X.Pa /etc/rc.d/cwebsockify
Xwhen the package is installed.
XIt is called
X.Sy cwebsockify
Xand not
X.Sy websockify
Xbecause
X.Pa mate/noVNC
Xinstalls a script of the latter name for its own service; the two are
Xseparate services and may run together.
XEnable it with
X.Ql cwebsockify=YES
Xin
X.Xr rc.conf 5 ;
Xthe script's own comments list the variables it reads.
X.El
X.Sh EXAMPLES
XServe the noVNC client and relay to a VNC server on the same host:
X.Bd -literal -offset indent
Xwebsockify --web /usr/pkg/share/noVNC 6080 localhost:5900
X.Ed
X.Pp
XThe same, holding the certificate itself and refusing plaintext:
X.Bd -literal -offset indent
Xwebsockify --web /usr/pkg/share/noVNC \e
X    --cert /etc/openssl/certs/vnc.pem --ssl-only \e
X    6080 localhost:5900
X.Ed
X.Pp
XRoute each connection by token, with the client served by a reverse
Xproxy in front:
X.Bd -literal -offset indent
Xwebsockify --token-plugin TokenFile \e
X    --token-source /var/gtkweb/tokens 127.0.0.1:6090
X.Ed
X.Pp
XThe same driven from
X.Xr rc.subr 8 ,
Xas an unprivileged user, with a pidfile the service can manage:
X.Bd -literal -offset indent
Xwebsockify --token-plugin TokenFile \e
X    --token-source /var/gtkweb/tokens \e
X    --log-file /var/gtkweb/log/websockify.log \e
X    --pidfile /var/gtkweb/gtkweb_ws.pid \e
X    -D 127.0.0.1:6090
X.Ed
X.Sh COMPATIBILITY
X.Fl -inetd ,
X.Fl -wrap-mode ,
X.Fl -record ,
X.Fl -auth-plugin ,
X.Fl -ssl-target ,
X.Fl -ssl-version ,
X.Fl -ssl-ciphers ,
X.Fl -key-password
Xand the JWT, Redis and HTTP API token plugins of the Python
Ximplementation are not provided.
XEach is rejected with a message naming the alternative rather than being
Xignored.
X.Pp
X.Fl -cert
Xhas no default here.
XThe Python implementation looks for
X.Pa self.pem
Xin the working directory and turns TLS on if it finds one; this one
Xserves TLS only when told to.
X.Sh SEE ALSO
X.Xr Xvnc 1 ,
X.Xr daemon 3 ,
X.Xr nginx 8 ,
X.Xr rc.subr 8
X.Sh AUTHORS
XThe protocol and the command line follow websockify from the noVNC
Xproject.
XThis implementation was written for
X.Nx
Xby
X.An Ricardo Lenzi Aq Mt rc_lenzi%yahoo.com.br@localhost .
END-of-cwebsockify/files/websockify.1
echo x - cwebsockify/files/token.h
sed 's/^X//' >cwebsockify/files/token.h << 'END-of-cwebsockify/files/token.h'
X// SPDX-License-Identifier: LGPL-3.0-or-later
X// Copyright (c) 2026 Ricardo Lenzi
X
X#ifndef WEBSOCKIFY_TOKEN_H
X#define WEBSOCKIFY_TOKEN_H
X
X#include <glib.h>
X
Xtypedef struct {
X	gchar	*host;
X	guint16	 port;
X} token_target_t;
X
Xvoid		 token_init(const gchar *source);
Xvoid		 token_shutdown(void);
X
X/*
X * Resolve a token to its target.  Free the result with token_target_free().
X * NULL means the token is not known.
X *
X * Called from worker threads, so the cache behind it is mutex guarded.
X */
Xtoken_target_t	*token_lookup(const gchar *token);
Xvoid		 token_target_free(token_target_t *t);
X
X#endif /* WEBSOCKIFY_TOKEN_H */
END-of-cwebsockify/files/token.h
echo x - cwebsockify/files/http.h
sed 's/^X//' >cwebsockify/files/http.h << 'END-of-cwebsockify/files/http.h'
X// SPDX-License-Identifier: LGPL-3.0-or-later
X// Copyright (c) 2026 Ricardo Lenzi
X
X#ifndef WEBSOCKIFY_HTTP_H
X#define WEBSOCKIFY_HTTP_H
X
X#include <gio/gio.h>
X#include <glib.h>
X
Xtypedef struct {
X	gchar		*path;		/* query stripped */
X	GHashTable	*query;		/* name -> value */
X	GHashTable	*headers;	/* lowercased name -> value */
X	gboolean	 is_upgrade;
X} http_req_t;
X
Xgboolean	 http_read_request(GInputStream *in, http_req_t *req,
X		    GCancellable *cancel, GError **error);
Xvoid		 http_req_clear(http_req_t *req);
X
Xconst gchar	*http_header(const http_req_t *req, const gchar *name);
Xconst gchar	*http_param(const http_req_t *req, const gchar *name);
X
Xgboolean	 http_serve_file(GOutputStream *out, const gchar *webroot,
X		    const gchar *path, GCancellable *cancel, GError **error);
Xvoid		 http_send_error(GOutputStream *out, guint code,
X		    const gchar *reason, GCancellable *cancel);
X
X#endif /* WEBSOCKIFY_HTTP_H */
END-of-cwebsockify/files/http.h
echo x - cwebsockify/files/conn.h
sed 's/^X//' >cwebsockify/files/conn.h << 'END-of-cwebsockify/files/conn.h'
X// SPDX-License-Identifier: LGPL-3.0-or-later
X// Copyright (c) 2026 Ricardo Lenzi
X
X#ifndef WEBSOCKIFY_CONN_H
X#define WEBSOCKIFY_CONN_H
X
X#include <gio/gio.h>
X#include <glib.h>
X
Xtypedef struct {
X	const gchar	*web;		/* --web, or NULL */
X	const gchar	*token_source;	/* --token-source, or NULL */
X	const gchar	*unix_target;	/* --unix-target, or NULL */
X	gchar		*target_host;	/* fixed target, or NULL */
X	guint16		 target_port;
X	guint		 timeout;	/* seconds allowed for the request */
X	gboolean	 ssl_only;	/* --ssl-only: refuse plain clients */
X} conn_config_t;
X
Xtypedef struct {
X	GSocketConnection *client;
X	GCancellable	  *cancel;
X	const conn_config_t *cfg;
X
X	/*
X	 * The stream the request is read from and the reply written to:
X	 * the connection itself, or a GTlsServerConnection wrapping it.
X	 * Everything above the socket goes through this.
X	 */
X	GIOStream	 *stream;
X
X	/*
X	 * Set by the worker when the request has been dealt with, read by
X	 * the deadline timer running in the main loop: two threads, so it
X	 * is taken under the mutex.
X	 */
X	GMutex		 lock;
X	gboolean	 settled;
X	guint		 timer_id;
X
X	/*
X	 * The worker and the timer source each hold a reference.  Without
X	 * that the worker could free the connection while the timer was
X	 * being dispatched in the main loop, or leave the timer armed on
X	 * memory it had just given back.
X	 */
X	grefcount	 refs;
X} conn_t;
X
Xconn_t	*conn_new(GSocketConnection *client, const conn_config_t *cfg);
Xvoid	 conn_unref(conn_t *conn);
X
X/* Stop the request deadline applying: answered, relaying, or abandoned. */
Xvoid	 conn_settle(conn_t *conn);
X
X/* Runs on a GThreadPool worker; consumes the connection. */
Xvoid	 conn_serve(gpointer data, gpointer user_data);
X
X#endif /* WEBSOCKIFY_CONN_H */
END-of-cwebsockify/files/conn.h
echo c - cwebsockify
mkdir -p cwebsockify > /dev/null 2>&1
exit

>How-To-Repeat:

>Fix:




Home | Main Index | Thread Index | Old Index