jcs
/detritus
/amendments
/14
gopher: Add Gopher module
jcs made amendment 14 about 1 year ago
--- gopher.c Sat Oct 26 20:34:01 2024
+++ gopher.c Sat Oct 26 20:34:01 2024
@@ -0,0 +1,336 @@
+/*
+ * Copyright (c) 2024 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 <stdarg.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "detritus.h"
+
+#define GOPHER_PORT 70
+
+struct gopher_request {
+ struct browser *browser;
+ struct URI *uri;
+
+ unsigned char tcp_buf[(4 * 1500) + 2048]; /* 4*MTU + tcp_input */
+ char response[2048];
+ size_t response_len;
+ size_t total_response_len;
+
+ TCPiopb tcp_iopb;
+ StreamPtr tcp_stream;
+ wdsEntry tcp_wds[2];
+ TCPStatusPB tcp_status_pb;
+ bool tcp_done_reading;
+
+ char message[member_size(struct URI, path) + 3];
+ size_t message_len;
+};
+
+struct URI * gopher_parse_uri(char *uristr);
+void * gopher_init_request(struct browser *browser, struct URI *uri);
+bool gopher_process_request(void *cookie);
+void gopher_free(void *cookie);
+static bool parse_response(struct gopher_request *req);
+static void consume_response(struct gopher_request *req, size_t len);
+static void gopher_print(struct gopher_request *req, size_t len);
+
+struct request_handler gopher_handler = {
+ gopher_parse_uri,
+ gopher_init_request,
+ gopher_process_request,
+ gopher_free,
+};
+
+struct URI *
+gopher_parse_uri(char *uristr)
+{
+ return generic_parse_uri("gopher", uristr);
+}
+
+void *
+gopher_init_request(struct browser *browser, struct URI *uri)
+{
+ struct gopher_request *req = NULL;
+ char ip_s[12 + 3 + 1];
+ short err;
+ ip_addr ip, local_ip;
+ tcp_port port, local_port;
+
+ req = xmalloczero(sizeof(struct gopher_request));
+ if (req == NULL) {
+ warn("Out of memory");
+ goto fail;
+ }
+
+ req->browser = browser;
+ req->uri = uri;
+ if (req->uri->port == 0)
+ req->uri->port = GOPHER_PORT;
+
+ browser_statusf(browser, "Resolving \"%s\"...", req->uri->hostname);
+
+ err = DNSResolveName(req->uri->hostname, &ip, NULL);
+ if (err) {
+ browser_statusf(browser, "Error: Failed resolving: %d", err);
+ goto fail;
+ }
+
+ long2ip(ip, (char *)&ip_s);
+ browser_statusf(browser, "Connecting to %s port %d...", ip_s,
+ req->uri->port);
+
+ err = _TCPCreate(&req->tcp_iopb, &req->tcp_stream, (Ptr)req->tcp_buf,
+ sizeof(req->tcp_buf), NULL, NULL, NULL, false);
+ if (err) {
+ browser_statusf(browser, "Error: TCPCreate failed: %d", err);
+ goto fail;
+ }
+
+ err = _TCPActiveOpen(&req->tcp_iopb, req->tcp_stream, ip,
+ req->uri->port, &local_ip, &local_port, NULL, NULL, false);
+ if (err) {
+ browser_statusf(browser,
+ "Error: Failed connecting to %s (%s) port %d: %d",
+ req->uri->hostname, ip_s, req->uri->port, err);
+ goto fail;
+ }
+
+ err = _TCPStatus(&req->tcp_iopb, req->tcp_stream, &req->tcp_status_pb,
+ NULL, NULL, false);
+ if (err) {
+ browser_statusf(browser,
+ "Error: Failed TCPStatus on connection to %s (%s) port %d: %d",
+ req->uri->hostname, ip_s, req->uri->port, err);
+ goto fail;
+ }
+
+ /* send selector without leading slash */
+ req->message_len = snprintf(req->message, sizeof(req->message),
+ "%s\r\n", req->uri->path[1] == '\0' ? "" : req->uri->path + 1);
+
+ browser_statusf(browser, "Connected to %s, sending request...",
+ req->uri->hostname);
+
+ memset(&req->tcp_wds, 0, sizeof(req->tcp_wds));
+ req->tcp_wds[0].ptr = (Ptr)req->message;
+ req->tcp_wds[0].length = req->message_len;
+
+ err = _TCPSend(&req->tcp_iopb, req->tcp_stream, req->tcp_wds, NULL,
+ NULL, false);
+ if (err) {
+ browser_statusf(req->browser, "Error: TCPSend failed: %d", err);
+ goto fail;
+ }
+
+ return req;
+
+fail:
+ if (req->tcp_stream) {
+ _TCPAbort(&req->tcp_iopb, req->tcp_stream, NULL, NULL, false);
+ _TCPRelease(&req->tcp_iopb, req->tcp_stream, NULL, NULL, false);
+ }
+
+ if (req)
+ xfree(&req);
+
+ return NULL;
+}
+
+void
+gopher_free(void *cookie)
+{
+ struct gopher_request *req = (struct gopher_request *)cookie;
+
+ if (req->tcp_stream) {
+ _TCPAbort(&req->tcp_iopb, req->tcp_stream, NULL, NULL, false);
+ _TCPRelease(&req->tcp_iopb, req->tcp_stream, NULL, NULL, false);
+ }
+
+ xfree(&req->uri);
+ xfree(&req);
+}
+
+bool
+gopher_process_request(void *cookie)
+{
+ struct gopher_request *req = (struct gopher_request *)cookie;
+ size_t len, n;
+ short err;
+ unsigned short slen;
+
+ if (req->tcp_iopb.ioResult > 0 || CommandPeriodPressed()) {
+ BROWSER_DEBUGF((req->browser, "TCP I/O Result %d, disconnecting",
+ req->tcp_iopb.ioResult));
+ return false;
+ }
+
+ if (req->response_len < sizeof(req->response) &&
+ !req->tcp_done_reading) {
+ err = _TCPStatus(&req->tcp_iopb, req->tcp_stream,
+ &req->tcp_status_pb, NULL, NULL, false);
+ if (err) {
+ browser_statusf(req->browser, "Error: Bad TCPStatus: %d", err);
+ return false;
+ }
+ if (req->tcp_status_pb.connectionState !=
+ ConnectionStateEstablished) {
+ BROWSER_DEBUGF((req->browser, "TCP connection closed "
+ "(state %d)", req->tcp_status_pb.connectionState));
+ req->tcp_done_reading = true;
+ goto parse;
+ }
+
+ if (req->tcp_status_pb.amtUnreadData == 0)
+ goto parse;
+
+ slen = MIN(req->tcp_status_pb.amtUnreadData,
+ sizeof(req->response) - req->response_len);
+ if (!slen) {
+ browser_statusf(req->browser,
+ "Error: No buffer space available in response?");
+ return false;
+ }
+
+ err = _TCPRcv(&req->tcp_iopb, req->tcp_stream,
+ (Ptr)(req->response + req->response_len), &slen, NULL, NULL,
+ false);
+ if (err) {
+ browser_statusf(req->browser, "Error: Failed TCPRcv: %d", err);
+ goto parse;
+ }
+ req->response_len += slen;
+ req->total_response_len += slen;
+ BROWSER_DEBUGF((req->browser, "Read %d bytes of TCP data", slen));
+ browser_statusf(req->browser,
+ "Read %ld bytes", req->total_response_len);
+ }
+
+parse:
+ if (req->response_len)
+ return parse_response(req);
+
+ return true;
+}
+
+static bool
+parse_response(struct gopher_request *req)
+{
+ long n;
+
+restart_parse:
+ for (n = 0; n < req->response_len; n++) {
+ if (n + 3 <= req->response_len && req->response[0] == '.' &&
+ req->response[1] == '\r' && req->response[2] == '\n') {
+ /* end of transfer */
+ browser_statusf(req->browser,
+ "Finished reading %ld bytes", req->total_response_len);
+ return false;
+ }
+
+ if (n > 1 && req->response[n - 1] == '\r' &&
+ req->response[n] == '\n') {
+ req->response[n - 1] = '\0';
+ gopher_print(req, n + 1);
+ consume_response(req, n + 1);
+ goto restart_parse;
+ }
+ }
+
+ return true;
+}
+
+static void
+consume_response(struct gopher_request *req, size_t len)
+{
+ if (len == req->response_len) {
+ req->response_len = 0;
+ memset(req->response, 0, sizeof(req->response));
+ } else {
+ memmove(req->response, req->response + len,
+ sizeof(req->response) - len);
+ req->response_len -= len;
+ memset(req->response + req->response_len, 0,
+ sizeof(req->response) - req->response_len);
+ }
+}
+
+static void
+gopher_print(struct gopher_request *req, size_t len)
+{
+ static char uri[member_size(struct URI, str)], prefix[5];
+ char type, *label, *selector = NULL, *hostname = NULL;
+ unsigned short port = 0;
+ long n;
+
+ type = req->response[0];
+ label = req->response + 1;
+
+ for (n = 1; n < len; n++) {
+ if (req->response[n] != '\t')
+ continue;
+
+ req->response[n] = '\0';
+ if (selector == NULL)
+ selector = req->response + n + 1;
+ else if (hostname == NULL)
+ hostname = req->response + n + 1;
+ else {
+ port = atoi(req->response + n + 1);
+ break;
+ }
+ }
+
+ req->browser->style = STYLE_PRE;
+
+ switch (type) {
+ case '0':
+ /* text file */
+ case '1':
+ /* gopher submenu */
+ case '2':
+ /* CCSO nameserver? */
+ case '7':
+ /* search */
+ case 'd':
+ /* document */
+
+ snprintf(prefix, sizeof(prefix), "[%c] ", type);
+ browser_print(req->browser, prefix, 4);
+
+ len = snprintf(uri, sizeof(uri), "gopher://%s%s", hostname,
+ selector);
+ browser_print_link(req->browser, uri, len, label, strlen(label));
+ browser_print(req->browser, "\r", 1);
+ break;
+ case 'i':
+ /* informational */
+ browser_print(req->browser, label, strlen(label));
+ browser_print(req->browser, "\r", 1);
+ break;
+ default:
+ browser_print(req->browser, "type: ", 0);
+ browser_print(req->browser, (char *)&type, 1);
+ browser_print(req->browser, " label:", 0);
+ browser_print(req->browser, label, 0);
+ browser_print(req->browser, " selector:", 0);
+ browser_print(req->browser, selector, 0);
+ browser_print(req->browser, " host:", 0);
+ browser_print(req->browser, hostname, 0);
+ browser_print(req->browser, "\r", 1);
+ }
+}
\ No newline at end of file