jcs
/subtext
/amendments
/490
nomodem: Introduce NoModem, an alternative file transfer protocol
Instead of using ZModem over an already-reliable connection, NoModem
just sends plain binary data with no encoding or checksumming.
jcs made amendment 490 about 1 year ago
--- nomodem.c Thu Apr 27 00:18:09 2023
+++ nomodem.c Thu Apr 27 00:18:09 2023
@@ -0,0 +1,319 @@
+/*
+ * 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 <string.h>
+#include "nomodem.h"
+
+struct nomodem_session *
+nomodem_send(struct session *s, FILE *fp, char *filename)
+{
+ struct nomodem_session *ns;
+
+ if (!fp)
+ return NULL;
+
+ ns = xmalloczero(sizeof(struct nomodem_session));
+ if (ns == NULL)
+ return NULL;
+
+ ns->mode = NOMODEM_MODE_SENDING;
+ ns->session = s;
+ ns->file = fp;
+ strlcpy(ns->file_name, filename, sizeof(ns->file_name));
+
+ fseek(ns->file, 0, SEEK_END);
+ ns->file_size = ftell(ns->file);
+ fseek(ns->file, 0, SEEK_SET);
+
+ /* initiator */
+ session_printf(ns->session, "%c%c%c%c%c%c",
+ (char)NOMODEM_START0, (char)NOMODEM_START1,
+ (char)NOMODEM_START2, (char)NOMODEM_START3,
+ (char)NOMODEM_VERSION, (char)NOMODEM_CMD_SEND);
+
+ /* file name size (byte), file name */
+ session_printf(ns->session, "%c%s",
+ (unsigned char)strlen(ns->file_name), ns->file_name);
+
+ /* 32-bit file size (big-endian) */
+ session_printf(ns->session, "%c%c%c%c",
+ (unsigned char)((ns->file_size >> 24) & 0xff),
+ (unsigned char)((ns->file_size >> 16) & 0xff),
+ (unsigned char)((ns->file_size >> 8) & 0xff),
+ (unsigned char)(ns->file_size & 0xff));
+ session_flush(ns->session);
+
+ ns->last_input = Time;
+ ns->need_header_ack = true;
+
+ return ns;
+}
+
+struct nomodem_session *
+nomodem_receive(struct session *s, char *path)
+{
+ struct nomodem_session *ns;
+
+ if (path == NULL)
+ return NULL;
+
+ ns = xmalloczero(sizeof(struct nomodem_session));
+ if (ns == NULL)
+ return NULL;
+
+ ns->mode = NOMODEM_MODE_RECEIVING;
+ ns->session = s;
+ ns->file_path = xstrdup(path);
+ if (ns->file_path == NULL) {
+ xfree(&ns);
+ return NULL;
+ }
+
+ ns->file = fopen(ns->file_path, "wb+");
+ if (ns->file == NULL) {
+ session_printf(ns->session, "[nomodem] Failed opening %s for writing",
+ ns->file_path);
+ xfree(&ns->file_path);
+ xfree(&ns);
+ return NULL;
+ }
+
+ /* initiator */
+ session_printf(ns->session, "%c%c%c%c%c%c",
+ (char)NOMODEM_START0, (char)NOMODEM_START1,
+ (char)NOMODEM_START2, (char)NOMODEM_START3,
+ (char)NOMODEM_VERSION, (char)NOMODEM_CMD_RECEIVE);
+ session_flush(ns->session);
+
+ ns->last_input = Time;
+ ns->need_header = true;
+
+ return ns;
+}
+bool
+nomodem_timed_out(struct nomodem_session *ns)
+{
+ if ((Time - ns->last_input) > NOMODEM_TIMEOUT)
+ return true;
+
+ return false;
+}
+
+unsigned short
+nomodem_get_char(struct nomodem_session *ns)
+{
+ unsigned char b;
+
+ while (session_get_char(ns->session, &b) == 0) {
+ if (ns->session->ending || nomodem_timed_out(ns))
+ return -1;
+ }
+
+ ns->last_input = Time;
+
+ return (short)b;
+}
+
+bool
+nomodem_continue(struct nomodem_session *ns)
+{
+ size_t size, rsize, pos;
+ short resp;
+ unsigned short file_name_size, file_name_pos;
+ unsigned char b;
+
+ switch (ns->mode) {
+ case NOMODEM_MODE_SENDING:
+ if (ns->need_header_ack) {
+ resp = nomodem_get_char(ns);
+ if (resp < 0)
+ return false;
+ size = ((unsigned long)(resp & 0xff)) << 24;
+
+ resp = nomodem_get_char(ns);
+ if (resp < 0)
+ return false;
+ size |= ((unsigned long)(resp & 0xff)) << 16;
+
+ resp = nomodem_get_char(ns);
+ if (resp < 0)
+ return false;
+ size |= ((unsigned long)(resp & 0xff)) << 8;
+
+ resp = nomodem_get_char(ns);
+ if (resp < 0)
+ return false;
+ size |= (unsigned long)(resp & 0xff);
+
+ if (size != ns->file_size) {
+ session_logf(ns->session,
+ "[nomodem] Header ack size %ld != file size %ld, "
+ "canceling", size, ns->file_size);
+ return false;
+ }
+
+ ns->need_header_ack = false;
+ }
+
+ while (ns->session->obuflen > 0) {
+ session_flush(ns->session);
+ if (ns->session->ending)
+ return false;
+ if (nomodem_timed_out(ns))
+ return false;
+ }
+
+ pos = ftell(ns->file);
+ if (feof(ns->file))
+ return false;
+ size = fread(ns->session->obuf + 4, 1,
+ sizeof(ns->session->obuf) - 4, ns->file);
+ if (size == 0)
+ return false;
+
+ ns->session->obuf[0] = (size >> 24) & 0xff;
+ ns->session->obuf[1] = (size >> 16) & 0xff;
+ ns->session->obuf[2] = (size >> 8) & 0xff;
+ ns->session->obuf[3] = size & 0xff;
+
+ ns->session->obuflen = size + 4;
+ session_flush(ns->session);
+
+ /* wait for ack */
+ resp = nomodem_get_char(ns);
+
+ if (resp != NOMODEM_ACK) {
+ session_logf(ns->session, "[nomodem] Failed ack at chunk "
+ "%ld/%ld: %d", pos, ns->file_size, resp);
+ return false;
+ }
+
+ if (feof(ns->file))
+ return false;
+ break;
+ case NOMODEM_MODE_RECEIVING:
+ if (ns->need_header) {
+ file_name_size = nomodem_get_char(ns);
+ if (file_name_size < 0)
+ return false;
+
+ /* get filename */
+ file_name_pos = 0;
+ while (file_name_size != 0) {
+ resp = nomodem_get_char(ns);
+ if (resp < 0)
+ return false;
+
+ if (file_name_pos < sizeof(ns->file_name) - 1) {
+ ns->file_name[file_name_pos++] = (char)resp;
+ ns->file_name[file_name_pos] = 0;
+ }
+ file_name_size--;
+ }
+
+ /* get file size */
+ resp = nomodem_get_char(ns);
+ if (resp < 0)
+ return false;
+ ns->file_size = ((unsigned long)(resp & 0xff)) << 24;
+
+ resp = nomodem_get_char(ns);
+ if (resp < 0)
+ return false;
+ ns->file_size |= ((unsigned long)(resp & 0xff)) << 16;
+
+ resp = nomodem_get_char(ns);
+ if (resp < 0)
+ return false;
+ ns->file_size |= ((unsigned long)(resp & 0xff)) << 8;
+
+ resp = nomodem_get_char(ns);
+ if (resp < 0)
+ return false;
+ ns->file_size |= (unsigned long)(resp & 0xff);
+
+ /* ack */
+ session_printf(ns->session, "%c%c%c%c",
+ (char)((ns->file_size >> 24) & 0xff),
+ (char)((ns->file_size >> 16) & 0xff),
+ (char)((ns->file_size >> 8) & 0xff),
+ (char)(ns->file_size & 0xff));
+
+ ns->need_header = false;
+ }
+
+ pos = ftell(ns->file);
+ if (pos >= ns->file_size)
+ return false;
+
+ /* get chunk size */
+ resp = nomodem_get_char(ns);
+ if (resp < 0)
+ return false;
+ size = ((unsigned long)(resp & 0xff)) << 24;
+
+ resp = nomodem_get_char(ns);
+ if (resp < 0)
+ return false;
+ size |= ((unsigned long)(resp & 0xff)) << 16;
+
+ resp = nomodem_get_char(ns);
+ if (resp < 0)
+ return false;
+ size |= ((unsigned long)(resp & 0xff)) << 8;
+
+ resp = nomodem_get_char(ns);
+ if (resp < 0)
+ return false;
+ size |= (unsigned long)(resp & 0xff);
+
+ /* TODO: read chunks directly from ibuf */
+ for (rsize = 0; rsize < size; rsize++) {
+ resp = nomodem_get_char(ns);
+ if (resp < 0)
+ return false;
+ b = resp;
+ if (fwrite(&b, 1, 1, ns->file) != 1) {
+ session_logf(ns->session, "[nomodem] Failed writing to %s",
+ ns->file_path);
+ return false;
+ }
+ }
+
+ session_printf(ns->session, "%c", (char)NOMODEM_ACK);
+ session_flush(ns->session);
+
+ pos = ftell(ns->file);
+ if (pos >= ns->file_size)
+ return false;
+ }
+
+ return true;
+}
+
+void
+nomodem_finish(struct nomodem_session **nsp)
+{
+ struct nomodem_session *ns = (struct nomodem_session *)*nsp;
+
+ if (ns->file_path != NULL)
+ xfree(&ns->file_path);
+
+ if (ns->file)
+ fclose(ns->file);
+
+ xfree(nsp);
+}
--- nomodem.h Wed Apr 26 21:24:47 2023
+++ nomodem.h Wed Apr 26 21:24:47 2023
@@ -0,0 +1,67 @@
+/*
+ * 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 __NOMODEM_H__
+#define __NOMODEM_H__
+
+#include "session.h"
+#include "util.h"
+
+/* handshake sequence */
+#define NOMODEM_START0 0x01
+#define NOMODEM_START1 0x00
+#define NOMODEM_START2 0x01
+#define NOMODEM_START3 0x00
+
+/* then a version */
+#define NOMODEM_VERSION 0x01
+
+/* then a command */
+#define NOMODEM_CMD_SEND 0x01 /* we are sending you a file */
+#define NOMODEM_CMD_RECEIVE 0x02 /* please upload a file to us */
+
+#define NOMODEM_ACK 0x01
+#define NOMODEM_CANCEL 0x03 /* ^C */
+
+#define NOMODEM_TIMEOUT 20
+
+enum {
+ NOMODEM_MODE_SENDING,
+ NOMODEM_MODE_RECEIVING
+};
+
+struct nomodem_session {
+ struct session *session;
+
+ short mode;
+ FILE *file;
+ unsigned long file_size;
+ char file_name[64];
+ char *file_path;
+ bool need_header;
+ bool need_header_ack;
+ unsigned long last_input;
+};
+
+struct nomodem_session * nomodem_send(struct session *s, FILE *fp,
+ char *filename);
+struct nomodem_session * nomodem_receive(struct session *s, char *path);
+bool nomodem_timed_out(struct nomodem_session *ns);
+bool nomodem_continue(struct nomodem_session *ns);
+void nomodem_finish(struct nomodem_session **nsp);
+unsigned short nomodem_get_char(struct nomodem_session *ns);
+
+#endif