/* * Copyright (c) 2021-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" enum { PARSE_STATE_HEADER, PARSE_STATE_BODY, PARSE_STATE_DOWNLOAD }; bool gemini_accept_uri(struct URI *uri); bool gemini_request_init(page_handle pageh); bool gemini_process(page_handle pageh); void gemini_reset(page_handle pageh); static bool parse_header(struct page *page, char *str, size_t len); struct page_handler gemini_handler = { gemini_accept_uri, gemini_request_init, page_queue_output, page_consume_data, page_request_cleanup, gemini_process, gemini_reset, }; bool gemini_accept_uri(struct URI *uri) { return (strcasecmp(uri->scheme, "gemini") == 0); } bool gemini_request_init(page_handle pageh) { struct page *page = *pageh; char *output; size_t output_len; if (page->uri->port == 0) page->uri->port = GEMINI_PORT; output_len = strlen(page->uri->str) + 2; output = xmalloc(output_len + 1); if (page->request->output == NULL) { warn("Out of memory"); return false; } snprintf(output, output_len + 1, "%s\r\n", page->uri->str); page->request = request_connect(page->browser, page->uri->hostname, page->uri->port, true, BLUESCSI_TLS_INIT_REQUEST_FLAG_NO_VERIFY); if (page->request == NULL) { xfree(&output); return false; } page->request->output = output; page->request->output_len = output_len; return true; } static bool gemini_process(page_handle pageh) { struct page *page = *pageh; size_t n, trail, skip, len; ssize_t tlen, link_len, title_len, j; char *link, *title, *tcontent; struct URI *newuri; short err; char c, *filename; bool newline, gemtext; if (page->content_pos == page->content_len) return PAGE_CAN_READ_MORE(page); if (page->parse_state == PARSE_STATE_HEADER) { for (n = page->content_pos; n < page->content_len; n++) { c = page->content[n]; if (!(c == '\n' && n && page->content[n - 1] == '\r')) continue; if (!parse_header(page, page->content + page->content_pos, n - page->content_pos - 2)) { BROWSER_DEBUGF((page->browser, "Header parsing failed, disconnecting")); return false; } page->header_len = n + 1; /* ignore any "; charset" after type */ if (strncasecmp(page->content_type, "text/gemini", 11) == 0 || strncasecmp(page->content_type, "text/plain", 10) == 0) { page->parse_state = PARSE_STATE_BODY; browser_commit_to_loading_page(page->browser); page->content_pos = page->header_len; } else { page->parse_state = PARSE_STATE_DOWNLOAD; filename = strrchr(page->uri->path, '/'); if (filename && filename[0] == '/') filename++; if (!browser_start_download(page->browser, filename, page->content + page->header_len, page->content_len - page->header_len)) return false; } /* gemini only has one header */ break; } } if (page->parse_state != PARSE_STATE_BODY) return true; gemtext = (strncasecmp(page->content_type, "text/gemini", 11) == 0); if (!gemtext) page->browser->style = STYLE_PRE; for (n = page->content_pos; n < page->content_len; n++) { if (page->content[n] != '\n' && !(n == page->content_len - 1 && !PAGE_CAN_READ_MORE(page))) continue; len = n - page->content_pos + 1; trail = 0; skip = 0; newline = false; if (page->content[n] == '\n') { len--; trail = 1; newline = true; if (n > 0 && page->content[n - 1] == '\r') { len--; trail++; } } else if (PAGE_CAN_READ_MORE(page)) /* no newline at the end and fetching, so wait for more data */ return true; if (!gemtext) goto print_line; /* check for pre first because the other styles can be in it */ if (page->content[page->content_pos] == '`' && page->content[page->content_pos + 1] == '`' && page->content[page->content_pos + 2] == '`') { /* ``` toggle */ if (page->browser->style & STYLE_PRE) page->browser->style = STYLE_NONE; else page->browser->style = STYLE_PRE; /* the rest of the line can be a description, ignore */ trail += len; len = 0; newline = false; goto print_line; } else if (page->browser->style & STYLE_PRE) goto print_line; if (page->content[page->content_pos] == '=' && page->content[page->content_pos + 1] == '>') { /* link */ link_len = 0; title_len = 0; link = NULL; title = NULL; skip = 2; len -= 2; tlen = len; tcontent = page->content + page->content_pos + skip; link = tcontent; /* skip leading whitespace */ while (len && (c = *tcontent) && (c == ' ' || c == '\t')) { tcontent++; link++; len--; } /* consume link */ while (len && (c = *tcontent) && c != ' ' && c != '\t') { tcontent++; len--; link_len++; } /* consume whitespace after link */ while (len && (c = *tcontent) && (c == ' ' || c == '\t')) { tcontent++; len--; } if (len) { title = tcontent; title_len = len; /* trim trailing whitespace from title */ while (title_len && (c = title[title_len - 1]) && (c == ' ' || c == '\t')) { title_len--; } if (title_len == 0) title = NULL; } newuri = build_relative_uri(page->uri, link, link_len); if (newuri) { browser_print_link(page->browser, newuri->str, strlen(newuri->str), title, title_len, true); xfree(&newuri); } else browser_print_link(page->browser, link, link_len, title, title_len, true); page->browser->style = STYLE_NONE; page->content_pos += skip + tlen + trail; continue; } if (page->content[page->content_pos] == '#' && page->content[page->content_pos + 1] == '#' && page->content[page->content_pos + 2] == '#') { /* ### h3 */ page->browser->style = STYLE_H3; skip = 3; len -= 3; while ((c = page->content[page->content_pos + skip]) && (c == ' ' || c == '\t')) { skip++; len--; } goto print_line; } if (page->content[page->content_pos] == '#' && page->content[page->content_pos + 1] == '#') { /* ## h2 */ page->browser->style = STYLE_H2; skip = 2; len -= 2; while ((c = page->content[page->content_pos + skip]) && (c == ' ' || c == '\t')) { skip++; len--; } goto print_line; } if (page->content[page->content_pos] == '#') { /* # h1 */ page->browser->style = STYLE_H1; skip = 1; len--; while ((c = page->content[page->content_pos + skip]) && (c == ' ' || c == '\t')) { skip++; len--; } goto print_line; } if (page->content[page->content_pos] == '*') { /* * list item */ page->browser->style = STYLE_LIST; skip = 1; len--; while ((c = page->content[page->content_pos + skip]) && (c == ' ' || c == '\t')) { skip++; len--; } browser_print(page->browser, " ¥Ê", 3, false); goto print_line; } if (page->content[page->content_pos] == '>') { /* > quote text */ page->browser->style = STYLE_QUOTE; skip = 1; len--; while ((c = page->content[page->content_pos + skip]) && (c == ' ' || c == '\t')) { skip++; len--; } goto print_line; } print_line: if (len) { browser_print(page->browser, page->content + page->content_pos + skip, len, newline); } else if (newline) { /* print blank lines in the default size */ page->browser->style = STYLE_NONE; browser_print(page->browser, "\r", 1, false); } page->content_pos += skip + len + trail; } if (!PAGE_CAN_READ_MORE(page) && page->content_pos < page->content_len) { browser_print(page->browser, page->content + page->content_pos, page->content_len - page->content_pos, false); page->content_pos = page->content_len; } return PAGE_CAN_READ_MORE(page); } void gemini_reset(page_handle pageh) { struct page *page = *pageh; /* restart at body */ page->parse_state = PARSE_STATE_BODY; page->content_pos = page->header_len; } static bool parse_header(struct page *page, char *str, size_t len) { char fail[32], *newuri, *encoded, *query; Str255 txt; GrafPtr win; DialogPtr dlg; Handle ihandle; Rect irect, dlgrect; size_t tlen; short status, hit, itype, ret, top; BROWSER_DEBUGF((page->browser, "Received header: %s", str)); if (!(str[0] >= '0' && str[0] <= '9' && str[1] >= '0' && str[1] <= '9' && str[2] == ' ')) { browser_statusf(page->browser, "Error: Malformed response %s", str); return false; } page->server_status = ((str[0] - '0') * 10) + (str[1] - '0'); memcpy(fail, str, MIN(len, sizeof(fail))); fail[MIN(len, sizeof(fail) - 1)] = '\0'; if (page->server_status >= 10 && page->server_status <= 19) { /* input requested */ GetPort(&win); if ((dlg = GetNewDialog(INPUT_DLOG_ID, NULL, (WindowPtr)-1)) == NULL) { warn("Can't find DLOG %d", INPUT_DLOG_ID); return false; } center_in_screen(dlg->portRect.right - dlg->portRect.left, dlg->portRect.bottom - dlg->portRect.top, true, &dlgrect); MoveWindow(dlg, dlgrect.left, dlgrect.top, true); snprintf((char *)txt, sizeof(txt), "Input requested from %s", page->uri->hostname); CtoPstr(txt); SetWTitle(dlg, txt); GetDItem(dlg, INPUT_DLOG_PROMPT_ID, &itype, &ihandle, &irect); tlen = len - 2; if (tlen >= sizeof(txt)) tlen = sizeof(txt) - 1; memcpy(txt + 1, str + 3, tlen); txt[0] = tlen; SetIText(ihandle, txt); ShowWindow(dlg); for (;;) { ModalDialog(ModalDialogFilter, &hit); if (hit == ok || hit == cancel) break; } if (hit == ok) { GetDItem(dlg, INPUT_DLOG_INPUT_ID, &itype, &ihandle, &irect); GetIText(ihandle, txt); PtoCstr(txt); } DisposDialog(dlg); SetPort(win); if (hit != ok) { browser_statusf(page->browser, "Canceled input"); return false; } /* make a new uri with this same one but send input as query */ encoded = uri_encode(txt); if (encoded == NULL) { browser_statusf(page->browser, "Error: Out of memory"); return false; } tlen = 1 + strlen(encoded); query = xmalloc(tlen + 1); if (query == NULL) { xfree(&encoded); browser_statusf(page->browser, "Error: Out of memory"); } snprintf(query, tlen + 1, "?%s", encoded); xfree(&encoded); page->redir_to = build_relative_uri(page->uri, query, tlen); xfree(&query); if (page->redir_to == NULL) browser_statusf(page->browser, "Error: Out of memory"); return false; } if (page->server_status >= 20 && page->server_status <= 29) { /* success */ if (len + 3 + 1 > sizeof(page->content_type)) len = sizeof(page->content_type) - 1; memcpy(page->content_type, str + 3, len - 2); page->content_type[len - 2] = '\0'; return true; } if (page->server_status >= 30 && page->server_status <= 39) { /* redirect */ page->redir_to = build_relative_uri(page->uri, str + 3, len - 2); /* TODO: infinite loop detection */ return false; } if (page->server_status >= 40 && page->server_status <= 49) { /* temp fail */ browser_statusf(page->browser, "Error: Server reported temporary " "failure: %s", fail); return false; } if (page->server_status >= 50 && page->server_status <= 59) { /* perm fail */ browser_statusf(page->browser, "Error: Server reported " "permanent failure: %s", fail); return false; } if (page->server_status >= 60 && page->server_status <= 69) { /* auth, not supported */ browser_statusf(page->browser, "Error: Auth not supported (%d)", status); return false; } browser_statusf(page->browser, "Error: Unsupported status: %s", fail); return false; }