jcs
/subtext
/amendments
/315
binkp: Add FidoNet binkp fetcher/parser
This connects to a binkp server, logs in, fetches outstanding files
and caches them to a "binkp" subdirectory, marks them "got", and then
scans each cached file to find ZIP files containing ".pkt" files,
which are then passed to fidopkt.
Next up will be to actually import those parsed packets into a bile
database for viewing like boards.
jcs made amendment 315 about 1 year ago
--- binkp.c Thu Feb 23 09:05:28 2023
+++ binkp.c Thu Feb 23 09:05:28 2023
@@ -0,0 +1,582 @@
+/*
+ * FidoNet Binkp
+ * https://www.ritlabs.com/binkp/
+ *
+ * Copyright (c) 2023 joshua stein <jcs@jcs.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <string.h>
+#include "binkp.h"
+#include "fidopkt.h"
+#include "logger.h"
+#include "subtext.h"
+#include "zip.h"
+
+/* #define BINKP_DEBUG */
+
+static struct binkp_connection *binkpc = NULL;
+static Str255 binkp_dir;
+
+void binkp_fetch(void);
+void binkp_ingest(void);
+bool binkp_zip_decider(char *filename, size_t size);
+void binkp_zip_processor(char *filename, size_t size, unsigned char *data);
+
+void
+binkp_thread(struct uthread *uthread, void *arg)
+{
+ short newdirid, error;
+
+ /* make sure working dir is there */
+ if (getpath(db->bile->vrefnum, db->bile->filename, &binkp_dir,
+ false) != 0)
+ panic("getpath failed on %s", PtoCstr(db->bile->filename));
+ PtoCstr(binkp_dir);
+ strlcat((char *)binkp_dir, ":binkp", sizeof(binkp_dir));
+ CtoPstr(binkp_dir);
+
+ if (!FIsDir(binkp_dir)) {
+ error = DirCreate(db->bile->vrefnum, 0, binkp_dir, &newdirid);
+ if (error)
+ panic("Failed creating %s: %d", PtoCstr(binkp_dir), error);
+ }
+
+ for (;;) {
+ if (db->config.binkp_hostname[0] == '\0' ||
+ db->config.binkp_port == 0)
+ break;
+
+ binkp_fetch();
+ binkp_ingest();
+
+ if (!db->config.binkp_interval_seconds)
+ break;
+
+ uthread_msleep(1000UL * db->config.binkp_interval_seconds);
+ }
+}
+
+void
+binkp_atexit(void)
+{
+ if (binkpc != NULL)
+ binkp_connection_free(&binkpc);
+}
+
+void
+binkp_fetch(void)
+{
+ binkpc = binkp_connect(db->config.binkp_hostname,
+ db->config.binkp_port);
+ if (!binkpc)
+ return;
+
+ if (!binkp_login(binkpc))
+ return;
+
+ while (binkpc != NULL && !binkpc->done) {
+ binkp_read_frame(binkpc);
+ if (binkpc == NULL)
+ break;
+ uthread_yield();
+ }
+
+ if (binkpc != NULL)
+ binkp_connection_free(&binkpc);
+}
+
+struct binkp_connection *
+binkp_connect(char *hostname, unsigned short port)
+{
+ struct binkp_connection *conn;
+ char ip_s[16];
+ ip_addr local_ip, host_ip;
+ tcp_port local_port;
+ short error;
+
+ if (_TCPInit() != noErr)
+ panic("Failed initializing MacTCP");
+
+ conn = xmalloczero(sizeof(struct binkp_connection), "binkp_connection");
+ conn->buf_size = 1024;
+ conn->buf = xmalloc(conn->buf_size, "binkp buf");
+ conn->tcp_buf_size = (4 * 1500) + conn->buf_size;
+ conn->tcp_buf = xmalloc(conn->tcp_buf_size, "binkp tcp buf");
+
+ error = TCPResolveName(&hostname, &host_ip);
+ if (error) {
+ warn("Couldn't resolve host %s (%d)", hostname, error);
+ goto error;
+ }
+
+ error = _TCPCreate(&conn->tcp_send_iopb, &conn->tcp_stream,
+ (Ptr)conn->tcp_buf, conn->tcp_buf_size, nil, nil, nil, false);
+ if (error) {
+ warn("TCPCreate failed: %d", error);
+ goto error;
+ }
+
+ long2ip(host_ip, (char *)&ip_s);
+
+ logger_printf("[binkp] connecting to %s (%s) port %d",
+ hostname, ip_s, port);
+
+ error = _TCPActiveOpen(&conn->tcp_send_iopb, conn->tcp_stream,
+ host_ip, port, &local_ip, &local_port, nil, nil, false);
+ if (error) {
+ warn("Failed connecting to %s (%s) port %d: %d",
+ hostname, ip_s, port, error);
+ goto error;
+ }
+
+ return conn;
+
+error:
+ binkp_connection_free(&conn);
+ return NULL;
+}
+
+void
+binkp_connection_free(struct binkp_connection **ptr)
+{
+ struct binkp_connection *conn = (struct binkp_connection *)*ptr;
+
+ if (conn == NULL)
+ return;
+
+ if (conn->tcp_stream)
+ _TCPRelease(&conn->tcp_read_iopb, conn->tcp_stream, nil, nil,
+ false);
+
+ xfree(&conn->tcp_buf);
+ xfree(&conn->buf);
+
+ xfree(ptr);
+}
+
+size_t
+binkp_send_command(struct binkp_connection *conn, char *data,
+ size_t data_size)
+{
+ u_int16_t flen;
+ short error;
+
+ if (conn == NULL)
+ return 0;
+
+ if (data_size + 2 + 1 > conn->buf_size) {
+ warn("binkp_connection_send_frame: frame too large");
+ return 0;
+ }
+
+ while (conn->tcp_send_iopb.ioResult > 0)
+ /* previous _TCPSend has not completed yet */
+ uthread_yield();
+
+ flen = data_size;
+ /* BINKP_TYPE_COMMAND */
+ flen |= 0x8000;
+
+ conn->buf[0] = (flen >> 8) & 0xff;
+ conn->buf[1] = flen & 0xff;
+
+ memcpy(conn->buf + 2, data, data_size);
+ /* not sent, just for debugging */
+ conn->buf[2 + data_size] = '\0';
+
+ memset(&conn->tcp_wds, 0, sizeof(conn->tcp_wds));
+ conn->tcp_wds[0].ptr = (Ptr)conn->buf;
+ conn->tcp_wds[0].length = data_size + 2;
+
+#ifdef BINKP_DEBUG
+ logger_printf("[binkp] sending command of size %ld: %s", data_size,
+ conn->buf + 3);
+#endif
+
+ error = _TCPSend(&conn->tcp_send_iopb, conn->tcp_stream, conn->tcp_wds,
+ nil, nil, false);
+ if (error) {
+ logger_printf("[binkp] TCPSend of %d failed: %d",
+ conn->tcp_wds[0].length, error);
+ return 0;
+ }
+
+ return conn->tcp_wds[0].length;
+}
+
+bool
+binkp_read_frame(struct binkp_connection *conn)
+{
+ char tmp[128];
+ struct binkp_frame *frame;
+ size_t len, off, frame_data_read;
+ unsigned short rlen;
+ short error;
+ Ptr read_dest;
+
+ if (conn == NULL)
+ return false;
+
+ error = _TCPStatus(&conn->tcp_read_iopb, conn->tcp_stream,
+ &conn->tcp_status_pb, nil, nil, false);
+ if (error)
+ return false;
+
+ if (conn->tcp_status_pb.amtUnreadData < 2)
+ return false;
+
+ rlen = 2;
+ error = _TCPRcv(&conn->tcp_read_iopb, conn->tcp_stream,
+ (Ptr)&conn->cur_frame, &rlen, nil, nil, false);
+ if (error)
+ return false;
+
+ if (conn->cur_frame.data_size == 0)
+ warn("bogus frame?");
+
+ conn->cur_frame.type = (conn->cur_frame.data_size & (1 << 15)) == 0 ?
+ BINKP_TYPE_DATA : BINKP_TYPE_COMMAND;
+ conn->cur_frame.data_size &= 0x7fff;
+
+ frame_data_read = 0;
+ while (frame_data_read < conn->cur_frame.data_size) {
+ error = _TCPStatus(&conn->tcp_read_iopb, conn->tcp_stream,
+ &conn->tcp_status_pb, nil, nil, false);
+ if (error)
+ goto failed_read;
+
+ if (conn->tcp_status_pb.amtUnreadData == 0) {
+ uthread_yield();
+ continue;
+ }
+
+ if (conn->cur_frame.type == BINKP_TYPE_COMMAND) {
+ if (frame_data_read >= conn->buf_size) {
+ /*
+ * Frame is too big but we can't overwrite buf since we
+ * need the start of the frame, so just read into a junk
+ * buffer and discard it
+ */
+ read_dest = tmp;
+ rlen = MIN(conn->cur_frame.data_size - frame_data_read,
+ sizeof(tmp));
+ } else {
+ read_dest = conn->buf + frame_data_read;
+ rlen = MIN(conn->buf_size - frame_data_read,
+ conn->cur_frame.data_size - frame_data_read);
+ }
+ } else {
+ read_dest = conn->buf;
+ rlen = MIN(conn->cur_frame.data_size - frame_data_read,
+ conn->buf_size);
+ }
+
+ if (rlen > conn->tcp_status_pb.amtUnreadData)
+ rlen = conn->tcp_status_pb.amtUnreadData;
+
+ error = _TCPRcv(&conn->tcp_read_iopb, conn->tcp_stream,
+ read_dest, &rlen, nil, nil, false);
+ if (error)
+ goto failed_read;
+
+ frame_data_read += rlen;
+
+ if (conn->cur_frame.type == BINKP_TYPE_DATA) {
+ conn->cur_file.data_read += rlen;
+#ifdef BINKP_DEBUG
+ logger_printf("[binkp] read %d TCP chunk (%ld / %d)",
+ rlen, frame_data_read, conn->cur_frame.data_size);
+#endif
+
+ if (conn->cur_file.frefnum == 0)
+ panic("no frefnum for data, bogus state");
+ len = rlen;
+ error = FSWrite(conn->cur_file.frefnum, &len, conn->buf);
+ if (error) {
+ warn("error writing %d to %s: %d",
+ rlen, PtoCstr(conn->cur_file.pfilename), error);
+ CtoPstr(conn->cur_file.pfilename);
+ conn->done = true;
+ goto failed_read;
+ }
+ }
+ }
+
+ if (frame_data_read > conn->cur_frame.data_size)
+ panic("binkp data overread");
+
+ if (conn->cur_frame.type == BINKP_TYPE_COMMAND) {
+ conn->cur_frame.command_id = conn->buf[0];
+ if (frame_data_read < conn->buf_size)
+ conn->buf[frame_data_read] = '\0';
+ else
+ conn->buf[conn->buf_size - 1] = '\0';
+
+#ifdef BINKP_DEBUG
+ logger_printf("[binkp] read command 0x%x frame [%ld]: %s",
+ conn->cur_frame.command_id,
+ frame_data_read, conn->buf + (frame_data_read > 0 ? 1 : 0));
+#endif
+
+ switch (conn->cur_frame.command_id) {
+ case BINKP_COMMAND_M_NUL:
+ if (strncmp(conn->buf + 1, "SYS ", 4) == 0)
+ logger_printf("[binkp] fetching from %s",
+ conn->buf + 1 + 4);
+ break;
+ case BINKP_COMMAND_M_FILE:
+ if (conn->cur_file.filename[0]) {
+ logger_printf("[binkp] received M_FILE but not done "
+ "with file %s!", conn->cur_file.filename);
+ conn->cur_file.filename[0] = '\0';
+ if (conn->cur_file.frefnum > 0) {
+ FSClose(conn->cur_file.frefnum);
+ conn->cur_file.frefnum = 0;
+ }
+ }
+
+ if (sscanf(conn->buf + 1, "%128s %ld %ld %ld",
+ &conn->cur_file.filename, &conn->cur_file.size,
+ &conn->cur_file.mtime, &off) == 4) {
+ logger_printf("[binkp] new file \"%s\" size %ld",
+ conn->cur_file.filename, conn->cur_file.size);
+ if (off != 0)
+ warn("non-zero file fetch not supported");
+ conn->cur_file.data_read = 0;
+
+ PtoCstr(binkp_dir);
+ snprintf((char *)conn->cur_file.pfilename,
+ sizeof(conn->cur_file.pfilename),
+ "%s:%s", binkp_dir, conn->cur_file.filename);
+ CtoPstr(binkp_dir);
+ CtoPstr(conn->cur_file.pfilename);
+
+ error = Create(conn->cur_file.pfilename, 0,
+ SUBTEXT_CREATOR, 'BINK');
+ if (error == dupFNErr) {
+ FSDelete(conn->cur_file.pfilename, 0);
+ error = Create(conn->cur_file.pfilename, 0,
+ SUBTEXT_CREATOR, 'BINK');
+ }
+ if (error) {
+ warn("failed creating %s: %d",
+ PtoCstr(conn->cur_file.pfilename), error);
+ CtoPstr(conn->cur_file.pfilename);
+ goto failed_read;
+ }
+
+ error = FSOpen(conn->cur_file.pfilename, 0,
+ &conn->cur_file.frefnum);
+ if (error) {
+ warn("failed opening %s: %d",
+ PtoCstr(conn->cur_file.pfilename), error);
+ CtoPstr(conn->cur_file.pfilename);
+ goto failed_read;
+ }
+
+ len = conn->cur_file.size;
+ error = Allocate(conn->cur_file.frefnum, &len);
+ if (error) {
+ warn("failed setting %s to size %ld: %d",
+ PtoCstr(conn->cur_file.pfilename),
+ conn->cur_file.size, error);
+ CtoPstr(conn->cur_file.pfilename);
+ }
+ } else {
+ logger_printf("[binkp] failed parsing M_FILE: %s",
+ conn->buf + 1);
+ }
+ break;
+ case BINKP_COMMAND_M_EOB:
+ logger_printf("[binkp] end of batch, disconnecting");
+ conn->done = true;
+ break;
+ case BINKP_COMMAND_M_ERR:
+ logger_printf("[binkp] error from remote: %s", conn->buf + 1);
+ conn->done = true;
+ break;
+ case BINKP_COMMAND_M_BSY:
+ logger_printf("[binkp] remote is busy: %s", conn->buf + 1);
+ conn->done = true;
+ break;
+ }
+ } else {
+#ifdef BINKP_DEBUG
+ logger_printf("[binkp] read %d data frame of file (%ld / %ld)",
+ conn->cur_frame.data_size,
+ conn->cur_file.data_read, conn->cur_file.size);
+#endif
+ if (conn->cur_file.data_read == conn->cur_file.size) {
+ logger_printf("[binkp] done reading file %s (%ld)",
+ conn->cur_file.filename, conn->cur_file.size);
+
+ FSClose(conn->cur_file.frefnum);
+ conn->cur_file.frefnum = 0;
+
+ len = snprintf(tmp, sizeof(tmp), "%c%s %ld %ld",
+ BINKP_COMMAND_M_GOT, conn->cur_file.filename,
+ conn->cur_file.size, conn->cur_file.mtime);
+ if (!binkp_send_command(conn, tmp, len))
+ logger_printf("[binkp] failed sending M_GOT %s",
+ conn->cur_file.filename);
+
+ conn->cur_file.filename[0] = '\0';
+ }
+ }
+
+ return true;
+
+failed_read:
+ if (conn->cur_file.frefnum > 0) {
+ FSClose(conn->cur_file.frefnum);
+ conn->cur_file.frefnum = 0;
+ }
+ conn->done = true;
+ return false;
+}
+
+bool
+binkp_login(struct binkp_connection *conn)
+{
+ char command[50];
+ size_t len;
+
+ while (binkp_read_frame(conn))
+ ;
+
+ len = snprintf(command, sizeof(command), "%cSYS %s",
+ BINKP_COMMAND_M_NUL, db->config.name);
+ if (!binkp_send_command(conn, command, len))
+ return false;
+
+ len = snprintf(command, sizeof(command), "%cLOC %s",
+ BINKP_COMMAND_M_NUL, db->config.location);
+ if (!binkp_send_command(conn, command, len))
+ return false;
+
+ len = snprintf(command, sizeof(command), "%cNDL 28800,TCP,BINKP",
+ BINKP_COMMAND_M_NUL);
+ if (!binkp_send_command(conn, command, len))
+ return false;
+
+ len = snprintf(command, sizeof(command), "%cVER Subtext binkp/1.1",
+ BINKP_COMMAND_M_NUL);
+ if (!binkp_send_command(conn, command, len))
+ return false;
+
+ len = snprintf(command, sizeof(command), "%c%s",
+ BINKP_COMMAND_M_ADR, db->config.binkp_node_addr);
+ if (!binkp_send_command(conn, command, len))
+ return false;
+
+ len = snprintf(command, sizeof(command), "%c%s",
+ BINKP_COMMAND_M_PWD, db->config.binkp_password);
+ if (!binkp_send_command(conn, command, len))
+ return false;
+
+ for (;;) {
+ if (!conn)
+ return false;
+
+ if (!binkp_read_frame(conn)) {
+ uthread_yield();
+ continue;
+ }
+
+ if (conn->cur_frame.type != BINKP_TYPE_COMMAND)
+ continue;
+
+ if (conn->cur_frame.command_id == BINKP_COMMAND_M_OK) {
+ logger_printf("[binkp] logged in successfully as %s",
+ db->config.binkp_node_addr);
+ return true;
+ }
+
+ if (conn->cur_frame.command_id == BINKP_COMMAND_M_ERR) {
+ logger_printf("[binkp] login as %s failed, disconnecting",
+ db->config.binkp_node_addr);
+ binkp_connection_free(&conn);
+ return false;
+ }
+ }
+
+ return false;
+}
+
+void
+binkp_ingest(void)
+{
+ CInfoPBRec cipbr = { 0 };
+ HFileInfo *fpb = (HFileInfo *)&cipbr;
+ DirInfo *dpb = (DirInfo *)&cipbr;
+ short error, n;
+ Str255 file_name, abs_path;
+ short dir_id;
+
+ fpb->ioVRefNum = 0;
+ fpb->ioNamePtr = binkp_dir;
+ error = PBGetCatInfo(&cipbr, false);
+ if (error) {
+ warn("PBGetCatInfo on binkp dir failed: %d", error);
+ return;
+ }
+ dir_id = dpb->ioDrDirID;
+
+ fpb->ioNamePtr = file_name;
+ for (n = 1; ; n++) {
+ file_name[0] = 0;
+ fpb->ioDirID = dir_id;
+ fpb->ioFDirIndex = n;
+
+ error = PBGetCatInfo(&cipbr, false);
+ if (error)
+ break;
+
+ PtoCstr(binkp_dir);
+ snprintf((char *)abs_path, sizeof(abs_path), "%s:%s",
+ binkp_dir, PtoCstr(file_name));
+ CtoPstr(binkp_dir);
+ logger_printf("[binkp] scanning zip file %s", (char *)abs_path);
+
+ CtoPstr(abs_path);
+ zip_read_file(abs_path, binkp_zip_decider, binkp_zip_processor);
+ }
+}
+
+bool
+binkp_zip_decider(char *filename, size_t size)
+{
+ size_t flen = strlen(filename);
+
+ if (strcmp(filename + flen - 4, ".pkt") == 0)
+ return true;
+
+ return false;
+}
+
+void
+binkp_zip_processor(char *filename, size_t size, unsigned char *data)
+{
+ struct fidopkt *fidopkt;
+
+ uthread_yield();
+
+ fidopkt = fidopkt_parse(filename, (char *)data, size);
+ if (fidopkt == NULL)
+ return;
+
+ fidopkt_free(&fidopkt);
+}
--- binkp.h Thu Feb 23 08:48:55 2023
+++ binkp.h Thu Feb 23 08:48:55 2023
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2023 joshua stein <jcs@jcs.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef __BINKP_H__
+#define __BINKP_H__
+
+#include "tcp.h"
+#include "uthread.h"
+#include "util.h"
+
+struct binkp_frame {
+ u_int16_t data_size;
+ char command_id;
+#define BINKP_COMMAND_M_NUL 0
+#define BINKP_COMMAND_M_ADR 1
+#define BINKP_COMMAND_M_PWD 2
+#define BINKP_COMMAND_M_FILE 3
+#define BINKP_COMMAND_M_OK 4
+#define BINKP_COMMAND_M_EOB 5
+#define BINKP_COMMAND_M_GOT 6
+#define BINKP_COMMAND_M_ERR 7
+#define BINKP_COMMAND_M_BSY 8
+#define BINKP_COMMAND_M_GET 9
+#define BINKP_COMMAND_M_SKIP 10
+
+ char type;
+#define BINKP_TYPE_DATA 0x0
+#define BINKP_TYPE_COMMAND 0x1
+};
+
+struct binkp_file {
+ char filename[64];
+ size_t size;
+ unsigned long mtime;
+ size_t data_read;
+ Str255 pfilename;
+ short frefnum;
+};
+
+struct binkp_connection {
+ TCPiopb tcp_send_iopb;
+ TCPiopb tcp_read_iopb;
+ StreamPtr tcp_stream;
+ unsigned char *tcp_buf;
+ size_t tcp_buf_size;
+ wdsEntry tcp_wds[2];
+ TCPStatusPB tcp_status_pb;
+
+ char *buf;
+ size_t buf_size;
+ struct binkp_frame cur_frame;
+ struct binkp_file cur_file;
+ bool done;
+};
+
+void binkp_thread(struct uthread *uthread, void *arg);
+
+struct binkp_connection * binkp_connect(char *, unsigned short);
+void binkp_connection_free(struct binkp_connection **);
+void binkp_atexit(void);
+size_t binkp_send_frame(struct binkp_connection *conn,
+ struct binkp_frame *frame);
+size_t binkp_send_command(struct binkp_connection *conn, char *data,
+ size_t data_len);
+bool binkp_read_frame(struct binkp_connection *conn);
+bool binkp_login(struct binkp_connection *conn);
+
+#endif