AmendHub

Download:

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