/* * 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 { REQ_STATE_NEGOTIATING = 1, REQ_STATE_SENDING_REQUEST, REQ_STATE_PARSING_RESPONSE }; struct default_port { char *scheme; unsigned short port; } default_ports[] = { { "finger", FINGER_PORT }, { "gemini", GEMINI_PORT }, { "gopher", GOPHER_PORT }, { "http", HTTP_PORT }, { "https", HTTPS_PORT }, }; /* TCP functions */ bool request_tcp_connect(struct request *request); bool request_tcp_send(struct request *request, char *data, size_t len); ssize_t request_tcp_avail(struct request *request); ssize_t request_tcp_read(struct request *request, char *buf, size_t len); bool request_tls_init(struct request *request); void request_tls_cleanup(struct tls_request *request); ssize_t request_tls_read_tls_ciphertext(struct request *request); ssize_t request_tls_read_tcp_ciphertext(struct request *request, short space); ssize_t request_tls_send_plaintext(struct request *request, size_t space, request_data_queuer queuer, void *cookie); ssize_t request_tls_read_plaintext(struct request *request, request_data_consumer consumer, void *cookie); enum { URI_STATE_SCHEME, URI_STATE_HOSTNAME, URI_STATE_PORT, URI_STATE_PATH, URI_STATE_QUERY }; struct URI * parse_uri(char *uristr) { register char c; static char scheme[URI_MAX_SCHEME_LEN + 1]; static char hostname[URI_MAX_HOSTNAME_LEN + 1]; static char sport[URI_MAX_PORT_LEN + 1]; static char path[URI_MAX_PATH_LEN + 1]; static char query[URI_MAX_QUERY_LEN + 1]; static char str[URI_MAX_STR_LEN + 1]; struct URI *uri; char *data; size_t n, uri_len, scheme_len, hostname_len, sport_len, path_len, query_len, str_len, cat_len, size; short state, count, pos; long lport; unsigned short port; /* TODO: handle host:port */ /* TODO: handle //user:pass@host */ uri_len = strlen(uristr); scheme[0] = '\0'; hostname[0] = '\0'; port = 0; sport[0] = '\0'; path[0] = '\0'; query[0] = '\0'; str[0] = '\0'; state = URI_STATE_SCHEME; pos = 0; for (n = 0; n < uri_len; n++) { c = uristr[n]; switch (state) { case URI_STATE_SCHEME: if (pos == sizeof(scheme)) return NULL; if (c == ':') { if (uristr[n + 1] != '/' || uristr[n + 2] != '/') /* TODO: support "mailto:" type URIs? */ return NULL; scheme[pos] = '\0'; pos = 0; n += 2; state = URI_STATE_HOSTNAME; break; } if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) { scheme[pos++] = c; scheme[pos] = '\0'; } else return NULL; break; case URI_STATE_HOSTNAME: if (pos == sizeof(hostname)) return NULL; if (c == '/' || c == '?') { hostname[pos] = '\0'; pos = 0; if (c == '?') { state = URI_STATE_QUERY; n++; } else { state = URI_STATE_PATH; path[0] = '/'; path[1] = '\0'; pos = 1; } break; } if (c == ':') { hostname[pos] = '\0'; pos = 0; state = URI_STATE_PORT; break; } if ((c >= '0' && c <= '9') || c == '.' || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || ((c == '.' || c == '-') && pos != 0)) { hostname[pos++] = c; hostname[pos] = '\0'; } else return NULL; break; case URI_STATE_PORT: if (pos == sizeof(sport)) return NULL; if (c == '/' || c == '?') { sport[pos] = '\0'; pos = 0; if (c == '?') state = URI_STATE_QUERY; else { state = URI_STATE_PATH; path[0] = '/'; path[1] = '\0'; pos = 1; } break; } if (c >= '0' && c <= '9') { sport[pos++] = c; sport[pos] = '\0'; } else return NULL; break; case URI_STATE_PATH: if (pos == sizeof(path)) return NULL; if (c == '?') { path[pos] = '\0'; pos = 0; state = URI_STATE_QUERY; break; } /* XXX: should we encode non-ascii here? */ path[pos++] = c; path[pos] = '\0'; break; case URI_STATE_QUERY: if (pos == sizeof(path)) return NULL; query[pos++] = c; query[pos] = '\0'; break; } } if (state == URI_STATE_SCHEME) return NULL; if (hostname[0] == '\0') return NULL; if (sport[0]) { lport = atol(sport); if (lport == 0 || lport > USHRT_MAX) return NULL; port = lport; } /* if the uri has a port but it's the default for the scheme, drop it */ for (n = 0; n < sizeof(default_ports) / sizeof(default_ports[0]); n++) { if (strcasecmp(default_ports[n].scheme, scheme) == 0) { if (port == default_ports[n].port) { port = 0; sport[0] = '\0'; } break; } } if (path[0] == '\0') { path[0] = '/'; path[1] = '\0'; } scheme_len = strlen(scheme); hostname_len = strlen(hostname); sport_len = strlen(sport); path_len = strlen(path); query_len = strlen(query); str_len = scheme_len + 3 + hostname_len + path_len; if (port) str_len += 1 + sport_len; if (query_len) str_len += 1 + query_len; size = sizeof(struct URI) + scheme_len + 1 + hostname_len + 1 + path_len + 1 + query_len + 1 + str_len + 1; uri = xmalloc(size); if (uri == NULL) return NULL; uri->port = port; data = (char *)uri + sizeof(struct URI); uri->scheme = data; memcpy(uri->scheme, scheme, scheme_len); uri->scheme[scheme_len] = '\0'; data += scheme_len + 1; uri->hostname = data; memcpy(uri->hostname, hostname, hostname_len); uri->hostname[hostname_len] = '\0'; data += hostname_len + 1; uri->path = data; memcpy(uri->path, path, path_len); uri->path[path_len] = '\0'; data += path_len + 1; uri->query = data; memcpy(uri->query, query, query_len); uri->query[query_len] = '\0'; data += query_len + 1; uri->str = data; cat_len = strlcpy(data, uri->scheme, str_len + 1); cat_len = strlcat(data, "://", str_len + 1); cat_len = strlcat(data, uri->hostname, str_len + 1); if (port) { cat_len = strlcat(data, ":", str_len + 1); cat_len = strlcat(data, sport, str_len + 1); } cat_len = strlcat(data, path, str_len + 1); if (query_len) { cat_len = strlcat(data, "?", str_len + 1); cat_len = strlcat(data, query, str_len + 1); } if (cat_len > str_len) panic("URI overflow"); return uri; } struct URI * build_relative_uri(struct URI *uri, char *relative, size_t len) { static char path[URI_MAX_PATH_LEN + 1]; static char str[URI_MAX_STR_LEN + 1]; size_t plen, n; ssize_t slen; /* http://a.b/c + //d/e -> http://d/e */ if (relative[0] == '/' && relative[1] == '/') { /* retain scheme, new host, port, path, query */ slen = snprintf(str, sizeof(str), "%s:", uri->scheme); if (len > sizeof(str) - slen - 1) return NULL; memcpy(str + slen, relative, len); str[slen + len] = '\0'; return parse_uri(str); } /* if we can find :// before /, this looks like a whole new uri */ if (relative[0] != '/' && relative[0] != '?') { for (n = 0; n <= len; n++) { if (relative[n] == ':' && relative[n + 1] == '/' && relative[n + 2] == '/') { /* scheme, not relative */ if (len > sizeof(str) - 1) return NULL; memcpy(str, relative, len); str[len] = '\0'; return parse_uri(str); } if (relative[n] == '/') break; } } /* retain scheme, host, port, any following will set new path/query */ slen = snprintf(str, sizeof(str), "%s://%s", uri->scheme, uri->hostname); if (uri->port) slen += snprintf(str + slen, sizeof(str) - slen, ":%d", uri->port); if (len > sizeof(str) - slen - 1) return NULL; /* http://a.b/c + ?goose -> http://a.b/c?goose */ if (relative[0] == '?') { slen = strlcat(str, uri->path, sizeof(str)); if (len + 1 > sizeof(str) - slen - 1) return NULL; memcpy(str + slen, relative, len); str[slen + len] = '\0'; return parse_uri(str); } /* http://a.b/c + /d/e -> http://a.b/d/e */ if (relative[0] == '/') { /* new path and query */ if (len > sizeof(str) - slen - 1) return NULL; memcpy(str + slen, relative, len); str[slen + len] = '\0'; return parse_uri(str); } for (n = 0; n <= len; n++) { /* http://a.b/c/d.html + e/f.html -> http://a.b/c/e/f.html */ if (relative[n] == '/' || n == len) { /* remove last component in uri path up to slash */ plen = strlcpy(path, uri->path, sizeof(path)); while (plen && path[plen - 1] != '/') { path[plen - 1] = '\0'; plen--; } if (plen == 0) { path[0] = '/'; path[1] = '\0'; plen = 1; } if (plen >= sizeof(path) - 1) plen = sizeof(path) - 2; memcpy(path + plen, relative, len); path[plen + len] = '\0'; snprintf(str, sizeof(str), "%s://%s%s", uri->scheme, uri->hostname, path); return parse_uri(str); } } /* what the heck is this? */ Debugger(); return parse_uri(relative); } char * uri_encode(unsigned char *str) { char *ret = NULL; size_t len, n; bool encode = false; char a, b; encode: for (n = 0, len = 0; str[n] != '\0'; n++) { if ((str[n] >= 'A' && str[n] <= 'Z') || (str[n] >= 'a' && str[n] <= 'z') || (str[n] >= '0' && str[n] <= '9') || (str[n] == '-' || str[n] == '_' || str[n] == '.' || str[n] == '~')) { if (ret) ret[len] = str[n]; len++; } else { if (ret) { sprintf(ret + len, "%%%02X", str[n]); } len += 3; } } if (ret) { ret[len] = '\0'; return ret; } ret = xmalloc(len + 1); if (ret == NULL) { warn("Failed allocating %ld", len + 1); return NULL; } len = 0; goto encode; } struct request * request_connect(struct browser *browser, char *hostname, unsigned short port, bool tls, unsigned char tls_flags) { struct request *request; request = xmalloczero(sizeof(struct request)); if (request == NULL) return NULL; request->browser = browser; strlcpy(request->hostname, hostname, sizeof(request->hostname)); request->port = port; if (!request_tcp_connect(request)) { request_xfree(&request); return NULL; } if (tls) { request->tls_flags = tls_flags; if (!request_tls_init(request)) { request_xfree(&request); return NULL; } } return request; } void request_xfree(struct request **requestp) { struct request *request = *requestp; if (request->tls_id) scsi_tls_close(request->tls_id); if (request->stream) { _TCPRelease(&request->iopb, request->stream, NULL, NULL, false); request->stream = 0; } if (request->output) xfree(&request->output); xfree(requestp); } bool request_data_shuffle(struct request *request, request_data_queuer queuer, void *queuer_cookie, request_data_consumer consumer, void *consumer_cookie) { size_t len; ssize_t slen; char *data; short status, cipherspace, plainspace, tls_error; EventRecord event; do { if (request->tls_id) { status = scsi_tls_status(request->tls_id, &cipherspace, &plainspace, &tls_error); if ((status & 0x10) || (status & 0x1)) { /* tls has plaintext data for us */ slen = request_tls_read_plaintext(request, consumer, consumer_cookie); if (slen < 0) return false; if (slen == 0) status &= ~0x10; } if (status & 0x2) { /* tls has ciphertext for tcp */ slen = request_tls_read_tls_ciphertext(request); if (slen < 0) return false; if (slen == 0) status &= ~0x2; } if (status & 0x8) { /* tls can read plaintext from us */ slen = request_tls_send_plaintext(request, plainspace, queuer, queuer_cookie); if (slen < 0) return false; if (slen == 0) status &= ~0x8; } if (status & 0x4) { /* tls can read ciphertext from tcp */ slen = request_tls_read_tcp_ciphertext(request, cipherspace); if (slen < 0) return false; if (slen == 0) status &= ~0x4; } /* only do this when we have no other statuses */ if (status == 0x1) { /* closed */ if (tls_error != 0) browser_statusf(request->browser, "Error: TLS handshake failed: %d (TLS status " "0x%x)", tls_error, status); return false; } } else { status = 0; /* let the caller send out anything it has */ if (!queuer(request, queuer_cookie, &data, &len, false)) return false; if (len > 0 && data != NULL) { status = 1; if (!request_tcp_send(request, data, len)) return false; /* inform that we wrote len bytes */ if (!queuer(request, queuer_cookie, NULL, &len, true)) return false; } /* receive data and send it to the consumer */ slen = request_tcp_avail(request); if (slen < 0) { /* connection closed, let the consumer do one final read */ len = 0; consumer(request, consumer_cookie, &data, &len, true); return false; } if (slen > 0) { status = 1; /* get the consumer's buf to make sure it can handle slen */ data = NULL; len = slen; if (!consumer(request, consumer_cookie, &data, &len, false)) return false; /* read into their buf */ slen = request_tcp_read(request, data, len); if (slen < 0) { /* read failed, give one final pass for processing */ request->tcp_done_reading = true; len = 0; consumer(request, consumer_cookie, &data, &len, true); return false; } if (slen > 0) { /* and let them know we read it */ len = slen; if (!consumer(request, consumer_cookie, &data, &len, true)) return false; } } } if (request->tcp_done_reading && request->input_len == 0) return false; if (CommandPeriodPressed()) { browser_statusf(request->browser, "Request canceled"); return false; } /* if the user did anything else, let main event loop handle it */ if (GetNextEvent(everyEvent & ~updateMask, &event)) break; } while (status != 0); return true; } /* internal */ bool request_tcp_connect(struct request *request) { size_t len; short err; ip_addr local_ip; tcp_port local_port; browser_statusf(request->browser, "Resolving \"%s\"...", request->hostname); err = DNSResolveName(request->hostname, &request->ip, NULL); if (err) { browser_statusf(request->browser, "Error: Failed resolving %s: %d", request->hostname, err); return false; } long2ip(request->ip, (char *)&request->ip_s); browser_statusf(request->browser, "Connecting to %s port %d...", request->ip_s, request->port); err = _TCPCreate(&request->iopb, &request->stream, (Ptr)request->buf, sizeof(request->buf), NULL, NULL, NULL, false); if (err) { browser_statusf(request->browser, "Error: TCPCreate failed: %d", err); return false; } err = _TCPActiveOpen(&request->iopb, request->stream, request->ip, request->port, &local_ip, &local_port, NULL, NULL, false); if (err) { browser_statusf(request->browser, "Error: Failed connecting to %s (%s) port %d: %d", request->hostname, request->ip_s, request->port, err); return false; } err = _TCPStatus(&request->iopb, request->stream, &request->status_pb, NULL, NULL, false); if (err) { browser_statusf(request->browser, "Error: Failed TCPStatus on connection to %s (%s) port %d: %d", request->hostname, request->ip_s, request->port, err); return false; } return true; } ssize_t request_tcp_avail(struct request *request) { short err; if (request->iopb.ioResult > 0) { BROWSER_DEBUGF((request->browser, "TCP I/O Result %d, disconnecting", request->iopb.ioResult)); return -1; } err = _TCPStatus(&request->iopb, request->stream, &request->status_pb, NULL, NULL, false); if (err) { browser_statusf(request->browser, "Error: Bad TCPStatus: %d", err); return -1; } if (request->status_pb.connectionState != ConnectionStateEstablished) { BROWSER_DEBUGF((request->browser, "TCP connection closed (state %d)", request->status_pb.connectionState)); return -1; } return request->status_pb.amtUnreadData; } bool request_tcp_send(struct request *request, char *data, size_t len) { short err; memset(&request->wds, 0, sizeof(request->wds)); request->wds[0].ptr = (Ptr)data; request->wds[0].length = len; err = _TCPSend(&request->iopb, request->stream, request->wds, NULL, NULL, false); if (err) { browser_statusf(request->browser, "Error: TCPSend failed: %d", err); return false; } return true; } ssize_t request_tcp_read(struct request *request, char *buf, size_t len) { short err; unsigned short slen; slen = len; err = _TCPRcv(&request->iopb, request->stream, (Ptr)buf, &slen, NULL, NULL, false); if (err) { browser_statusf(request->browser, "Error: Failed TCPRcv: %d", err); return -1; } return slen; } /* * TLS over TCP using BlueSCSI TLS Accelerator */ bool request_tls_init(struct request *request) { struct tls_init_request tls_req; unsigned long time; if (!scsi_can_do_tls()) { warn("No TLS accelerator found, cannot make TLS requests"); return false; } browser_statusf(request->browser, "Connected to %s, negotiating TLS...", request->hostname); memset(&tls_req, 0, sizeof(tls_req)); strlcpy(tls_req.hostname, request->hostname, sizeof(tls_req.hostname)); time = MAC_TO_UNIX_TIME(Time); tls_req.unix_time[0] = (time >> 24) & 0xff; tls_req.unix_time[1] = (time >> 16) & 0xff; tls_req.unix_time[2] = (time >> 8) & 0xff; tls_req.unix_time[3] = (time) & 0xff; tls_req.flags[1] = request->tls_flags; request->tls_state = REQ_STATE_NEGOTIATING; request->tls_id = scsi_tls_init(&tls_req); if (request->tls_id == 0) { browser_statusf(request->browser, "Error: TLS handshake failed!"); return false; } return true; } ssize_t request_tls_read_tls_ciphertext(struct request *request) { size_t len; unsigned char *buf; /* read ciphertext from TLS and send it out TCP */ if (request->tcp_done_reading) return 0; if (request_tcp_avail(request) < 0) { request->tcp_done_reading = true; return 0; } /* this will point buf to scsi's static buffer */ buf = NULL; len = scsi_tls_read(request->tls_id, &buf, 0, true); if (len == 0) return 0; if (buf == NULL) return -1; BROWSER_DEBUGF((request->browser, "Read %lu bytes of TLS ciphertext, forwarding to TCP", len)); /* result ignored? */ request_tcp_send(request, (char *)buf, len); return len; } ssize_t request_tls_read_tcp_ciphertext(struct request *request, short space) { size_t len, n; ssize_t slen; short err; /* read ciphertext from TCP and send it to TLS */ if (request->input_len == sizeof(request->input) || request->tcp_done_reading) goto forward_ciphertext; slen = request_tcp_avail(request); if (slen < 0) request->tcp_done_reading = true; if (slen <= 0) goto forward_ciphertext; slen = MIN(slen, sizeof(request->input) - request->input_len); if (!slen) { browser_statusf(request->browser, "Error: No buffer space available in TCP input?"); goto forward_ciphertext; } slen = request_tcp_read(request, (char *)(request->input + request->input_len), slen); if (slen < 0) goto forward_ciphertext; request->input_len += slen; BROWSER_DEBUGF((request->browser, "Read %ld bytes of TCP ciphertext, forwarding to TLS", slen)); forward_ciphertext: if (!request->input_len || !space) return 0; slen = MIN(request->input_len, space); BROWSER_DEBUGF((request->browser, "Forwarding %ld bytes of TCP ciphertext to TLS", slen)); len = scsi_tls_write(request->tls_id, request->input, slen, true); if (len == 0) { browser_statusf(request->browser, "Error: Failed forwarding %ld bytes of ciphertext to TLS", slen); return -1; } if (len == request->input_len) request->input_len = 0; else { memmove(request->input, request->input + len, request->input_len - len); request->input_len -= len; } BROWSER_DEBUGF((request->browser, "Wrote %ld bytes of TCP ciphertext to TLS, %ld left", len, request->input_len)); return len; } ssize_t request_tls_send_plaintext(struct request *request, size_t space, request_data_queuer queuer, void *cookie) { size_t olen; char *data; size_t len; /* send any plaintext from us to TLS */ /* queuer will set data and len to message content */ len = space; if (!queuer(request, cookie, &data, &len, false)) return -1; if (len == 0 || data == NULL) return 0; if (request->tls_state == REQ_STATE_NEGOTIATING) request->tls_state = REQ_STATE_SENDING_REQUEST; else if (request->tls_state != REQ_STATE_SENDING_REQUEST) { browser_statusf(request->browser, "Error: bogus state (%d) instead of SENDING_REQUEST, " "disconnecting", request->tls_state); return -1; } olen = MIN(len, space); len = scsi_tls_write(request->tls_id, (unsigned char *)data, olen, false); if (!len) { browser_statusf(request->browser, "Error: Failed sending %ld bytes of plaintext to TLS", olen); return -1; } /* inform that we wrote len bytes */ if (!queuer(request, cookie, NULL, &len, true)) return -1; BROWSER_DEBUGF((request->browser, "Wrote %ld bytes of plaintext to TLS", len)); return len; } ssize_t request_tls_read_plaintext(struct request *request, request_data_consumer consumer, void *cookie) { size_t len; char *buf; /* read as much plaintext from TLS as we can buffer */ len = 1024; if (!consumer(request, cookie, &buf, &len, false)) return -1; len = scsi_tls_read(request->tls_id, (unsigned char **)&buf, len, false); if (len == 0) return 0; if (!consumer(request, cookie, &buf, &len, true)) return -1; return len; }