/* * Copyright (c) 2024 joshua stein * * 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 #include #include #include "detritus.h" #define GOPHER_ICON_SICN_ID 1024 #define GOPHER_ICON_TEXT_ID 0 #define GOPHER_ICON_FOLDER_ID 1 #define GOPHER_ICON_MACARCHIVE_ID 2 #define GOPHER_ICON_DOSARCHIVE_ID 3 #define GOPHER_ICON_UNKNOWN_ID 4 #define GOPHER_ICON_SEARCH_ID 5 #define GOPHER_ICON_SOUND_ID 6 #define GOPHER_ICON_IMAGE_ID 7 #define GOPHER_ICON_DOC_ID 8 #define GOPHER_ICON_GOPHER_ID 10 static const char showable_types[] = "013i"; struct gopher_page { Handle sicn; }; bool gopher_accept_uri(struct URI *uri); bool gopher_request_init(page_handle pageh); bool gopher_process(page_handle pageh); void gopher_free(page_handle pageh); static void gopher_print_menu(struct page *page, char *line, size_t len); struct page_handler gopher_handler = { gopher_accept_uri, gopher_request_init, page_queue_output, page_consume_data, page_request_cleanup, gopher_process, NULL, gopher_free, }; bool gopher_accept_uri(struct URI *uri) { return (strcasecmp(uri->scheme, "gopher") == 0); } bool gopher_request_init(page_handle pageh) { struct page *page = *pageh; struct gopher_page *gopher; size_t selector_len, output_len; char *filename, *output; if (page->uri->port == 0) page->uri->port = GOPHER_PORT; /* * If we have a path other than /, assume the first character is the * type and should not be sent, per RFC 4266. */ if (page->uri->path[0] == '\0' || (page->uri->path[0] == '/' && page->uri->path[1] == '\0')) { page->content_type[0] = '1'; selector_len = 0; } else { /* /0/blah -> blah */ page->content_type[0] = page->uri->path[1]; selector_len = strlen(page->uri->path + 2); } output_len = selector_len + 2; output = xmalloc(output_len + 1); if (output == NULL) { warn("Out of memory"); return false; } snprintf(output, output_len + 1, "%s\r\n", selector_len ? page->uri->path + 2 : ""); gopher = xmalloc(sizeof(struct gopher_page)); if (gopher == NULL) { xfree(&output); warn("Out of memory"); return false; } gopher->sicn = Get1Resource('SICN', GOPHER_ICON_SICN_ID); if (gopher->sicn == NULL) { xfree(&output); xfree(&gopher); warn("Can't find Gopher SICN"); return false; } page->request = request_connect(page->browser, page->uri->hostname, page->uri->port, false, 0); if (page->request == NULL) { ReleaseResource(gopher->sicn); xfree(&gopher); xfree(&output); return false; } page->request->output_len = output_len; page->request->output = output; page->handler_cookie = gopher; /* XXX: try to detect server responding with "3" for error? */ if (strchr(showable_types, page->content_type[0]) == NULL) { if (selector_len == 2) filename = NULL; else { filename = strrchr(page->uri->path + 2, '/'); if (filename && filename[0] == '/') filename++; } if (!browser_start_download(page->browser, filename, NULL, 0)) return false; } else browser_commit_to_loading_page(page->browser); return true; } bool gopher_process(page_handle pageh) { struct page *page = *pageh; long n, plines = 0; bool ret = true; if (strchr(showable_types, page->content_type[0]) && page->content_len >= 3 && (page->content_len == 3 || page->content[page->content_len - 4] == '\n') && page->content[page->content_len - 3] == '.' && page->content[page->content_len - 2] == '\r' && page->content[page->content_len - 1] == '\n') { /* ".\r\n" is end of transfer */ page->content_len -= 3; ret = false; } if (page->content_pos == page->content_len) return PAGE_CAN_READ_MORE(page); if (page->content_type[0] == '1') { /* gopher menu */ for (n = page->content_pos; n < page->content_len; n++) { if (n > 0 && page->content[n - 1] == '\r' && page->content[n] == '\n') { /* print line */ gopher_print_menu(page, page->content + page->content_pos, n - page->content_pos); page->content_pos = n + 1; } } if (!PAGE_CAN_READ_MORE(page) && page->content_pos < page->content_len) { gopher_print_menu(page, page->content + page->content_pos, page->content_len - page->content_pos); page->content_pos = page->content_len; ret = false; } } else if (page->content_type[0] == '0') { /* text file, convert newlines and display */ ret = page_print_plaintext(pageh); } else { /* anything else, just show it as-is */ browser_print(page->browser, page->content + page->content_pos, page->content_len - page->content_pos, false); page->content_pos = page->content_len; ret = false; } return ret; } void gopher_free(page_handle pageh) { struct page *page = *pageh; struct gopher_page *gopher; gopher = (struct gopher_page *)page->handler_cookie; ReleaseResource(gopher->sicn); xfree(&page->handler_cookie); } static void gopher_print_menu(struct page *page, char *line, size_t len) { static char uri[URI_MAX_STR_LEN + 1]; struct gopher_page *gopher; BitMap icon; FontInfo sizing; char type, *label, *selector = NULL, *hostname = NULL; size_t tlen; unsigned short port = 0; short icon_off = -1; long n; type = line[0]; label = line + 1; gopher = (struct gopher_page *)page->handler_cookie; for (n = 1; n < len; n++) { if (line[n] != '\t') continue; line[n] = '\0'; if (selector == NULL) selector = line + n + 1; else if (hostname == NULL) hostname = line + n + 1; else { port = atoi(line + n + 1); break; } } page->browser->style = STYLE_PRE; switch (type) { case 'i': /* informational */ browser_print(page->browser, label, strlen(label), true); break; default: if (type == 'h' && strncmp(selector, "URL:", 4) == 0) tlen = strlcpy(uri, selector + 4, sizeof(uri)); else tlen = snprintf(uri, sizeof(uri), "gopher://%s/%c%s", hostname, type, selector); switch (type) { case '0': /* text file */ icon_off = GOPHER_ICON_TEXT_ID; break; case '1': /* submenu */ if (strcasecmp(hostname, page->uri->hostname) == 0) icon_off = GOPHER_ICON_FOLDER_ID; else icon_off = GOPHER_ICON_GOPHER_ID; break; case '3': /* error page */ icon_off = GOPHER_ICON_UNKNOWN_ID; break; case '5': /* dos */ icon_off = GOPHER_ICON_DOSARCHIVE_ID; break; case '4': /* mac binhex */ icon_off = GOPHER_ICON_MACARCHIVE_ID; break; case '7': /* search */ icon_off = GOPHER_ICON_SEARCH_ID; break; case '2': /* ccso nameserver? */ case '+': /* mirror? */ case '6': /* uuencoded */ case '8': /* telnet */ case '9': /* binary file */ case 'T': /* telnet 3270 */ case 'd': /* doc */ case 'h': /* html */ case 'r': /* rtf */ case 'P': /* pdf */ case 'X': /* xml */ icon_off = GOPHER_ICON_DOC_ID; break; case 'g': /* gif */ case 'I': /* image */ case 'p': /* png */ case ':': /* bitmap */ case ';': /* movie */ icon_off = GOPHER_ICON_IMAGE_ID; break; case '<': /* sound */ icon_off = GOPHER_ICON_SOUND_ID; break; default: icon_off = GOPHER_ICON_UNKNOWN_ID; } HLock(gopher->sicn); icon.baseAddr = (char *)*(gopher->sicn) + (icon_off * 32); icon.rowBytes = 2; SetRect(&icon.bounds, 0, 0, 12, 12); sizing.ascent = 10; sizing.descent = 2; sizing.widMax = 18; sizing.leading = 0; browser_print_bitmap(page->browser, &icon, &sizing); HUnlock(gopher->sicn); page->browser->style |= STYLE_BOLD; browser_print_link(page->browser, uri, tlen, label, strlen(label), true); page->browser->style &= ~(STYLE_BOLD); break; } for (n = 1; n < len; n++) { if (line[n] == '\0') line[n] = '\t'; } }