AmendHub

Download

jcs

/

detritus

/

gemini.c

 

(View History)

jcs   *: Use PAGE_CAN_READ_MORE macro in protocol handlers Latest amendment: 49 on 2024-11-21

1 /*
2 * Copyright (c) 2021-2024 joshua stein <jcs@jcs.org>
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
16
17 #include <stdarg.h>
18 #include <stdio.h>
19 #include <string.h>
20
21 #include "detritus.h"
22
23 enum {
24 PARSE_STATE_HEADER,
25 PARSE_STATE_BODY,
26 PARSE_STATE_DOWNLOAD
27 };
28
29 bool gemini_accept_uri(struct URI *uri);
30 bool gemini_request_init(page_handle pageh);
31 bool gemini_process(page_handle pageh);
32 void gemini_reset(page_handle pageh);
33
34 static bool parse_header(struct page *page, char *str, size_t len);
35
36 struct page_handler gemini_handler = {
37 gemini_accept_uri,
38 gemini_request_init,
39 page_queue_output,
40 page_consume_data,
41 page_request_cleanup,
42 gemini_process,
43 gemini_reset,
44 };
45
46 bool
47 gemini_accept_uri(struct URI *uri)
48 {
49 return (strcasecmp(uri->scheme, "gemini") == 0);
50 }
51
52 bool
53 gemini_request_init(page_handle pageh)
54 {
55 struct page *page = *pageh;
56 char *output;
57 size_t output_len;
58
59 if (page->uri->port == 0)
60 page->uri->port = GEMINI_PORT;
61
62 output_len = strlen(page->uri->str) + 2;
63 output = xmalloc(output_len + 1);
64 if (page->request->output == NULL) {
65 warn("Out of memory");
66 return false;
67 }
68 snprintf(output, output_len + 1, "%s\r\n", page->uri->str);
69
70 page->request = request_connect(page->browser, page->uri->hostname,
71 page->uri->port, true, BLUESCSI_TLS_INIT_REQUEST_FLAG_NO_VERIFY);
72 if (page->request == NULL) {
73 xfree(&output);
74 return false;
75 }
76 page->request->output = output;
77 page->request->output_len = output_len;
78
79 return true;
80 }
81
82 static bool
83 gemini_process(page_handle pageh)
84 {
85 struct page *page = *pageh;
86 size_t n, trail, skip, len;
87 ssize_t tlen, link_len, title_len, j;
88 char *link, *title, *tcontent;
89 struct URI *newuri;
90 short err;
91 char c, *filename;
92 bool newline, gemtext;
93
94 if (page->content_pos == page->content_len)
95 return PAGE_CAN_READ_MORE(page);
96
97 if (page->parse_state == PARSE_STATE_HEADER) {
98 for (n = page->content_pos; n < page->content_len; n++) {
99 c = page->content[n];
100
101 if (!(c == '\n' && n && page->content[n - 1] == '\r'))
102 continue;
103
104 if (!parse_header(page, page->content + page->content_pos,
105 n - page->content_pos - 2)) {
106 BROWSER_DEBUGF((page->browser,
107 "Header parsing failed, disconnecting"));
108 return false;
109 }
110
111 page->header_len = n + 1;
112
113 /* ignore any "; charset" after type */
114 if (strncasecmp(page->content_type, "text/gemini", 11) == 0 ||
115 strncasecmp(page->content_type, "text/plain", 10) == 0) {
116 page->parse_state = PARSE_STATE_BODY;
117 browser_commit_to_loading_page(page->browser);
118 page->content_pos = page->header_len;
119 } else {
120 page->parse_state = PARSE_STATE_DOWNLOAD;
121
122 filename = strrchr(page->uri->path, '/');
123 if (filename && filename[0] == '/')
124 filename++;
125
126 if (!browser_start_download(page->browser, filename,
127 page->content + page->header_len,
128 page->content_len - page->header_len))
129 return false;
130 }
131
132 /* gemini only has one header */
133 break;
134 }
135 }
136
137 if (page->parse_state != PARSE_STATE_BODY)
138 return true;
139
140 gemtext = (strncasecmp(page->content_type, "text/gemini", 11) == 0);
141 if (!gemtext)
142 page->browser->style = STYLE_PRE;
143
144 for (n = page->content_pos; n < page->content_len; n++) {
145 if (page->content[n] != '\n' &&
146 !(n == page->content_len - 1 && !PAGE_CAN_READ_MORE(page)))
147 continue;
148
149 len = n - page->content_pos + 1;
150 trail = 0;
151 skip = 0;
152 newline = false;
153
154 if (page->content[n] == '\n') {
155 len--;
156 trail = 1;
157 newline = true;
158
159 if (n > 0 && page->content[n - 1] == '\r') {
160 len--;
161 trail++;
162 }
163 } else if (PAGE_CAN_READ_MORE(page))
164 /* no newline at the end and fetching, so wait for more data */
165 return true;
166
167 if (!gemtext)
168 goto print_line;
169
170 /* check for pre first because the other styles can be in it */
171 if (page->content[page->content_pos] == '`' &&
172 page->content[page->content_pos + 1] == '`' &&
173 page->content[page->content_pos + 2] == '`') {
174 /* ``` toggle */
175 if (page->browser->style & STYLE_PRE)
176 page->browser->style = STYLE_NONE;
177 else
178 page->browser->style = STYLE_PRE;
179
180 /* the rest of the line can be a description, ignore */
181 trail += len;
182 len = 0;
183 newline = false;
184 goto print_line;
185 } else if (page->browser->style & STYLE_PRE)
186 goto print_line;
187
188 if (page->content[page->content_pos] == '=' &&
189 page->content[page->content_pos + 1] == '>') {
190 /* link */
191 link_len = 0;
192 title_len = 0;
193 link = NULL;
194 title = NULL;
195
196 skip = 2;
197 len -= 2;
198 tlen = len;
199
200 tcontent = page->content + page->content_pos + skip;
201 link = tcontent;
202
203 /* skip leading whitespace */
204 while (len && (c = *tcontent) && (c == ' ' || c == '\t')) {
205 tcontent++;
206 link++;
207 len--;
208 }
209
210 /* consume link */
211 while (len && (c = *tcontent) && c != ' ' && c != '\t') {
212 tcontent++;
213 len--;
214 link_len++;
215 }
216
217 /* consume whitespace after link */
218 while (len && (c = *tcontent) && (c == ' ' || c == '\t')) {
219 tcontent++;
220 len--;
221 }
222
223 if (len) {
224 title = tcontent;
225 title_len = len;
226
227 /* trim trailing whitespace from title */
228 while (title_len && (c = title[title_len - 1]) &&
229 (c == ' ' || c == '\t')) {
230 title_len--;
231 }
232
233 if (title_len == 0)
234 title = NULL;
235 }
236
237 newuri = build_relative_uri(page->uri, link, link_len);
238 if (newuri) {
239 browser_print_link(page->browser, newuri->str,
240 strlen(newuri->str), title, title_len, true);
241 xfree(&newuri);
242 } else
243 browser_print_link(page->browser, link, link_len, title,
244 title_len, true);
245 page->browser->style = STYLE_NONE;
246 page->content_pos += skip + tlen + trail;
247 continue;
248 }
249
250 if (page->content[page->content_pos] == '#' &&
251 page->content[page->content_pos + 1] == '#' &&
252 page->content[page->content_pos + 2] == '#') {
253 /* ### h3 */
254 page->browser->style = STYLE_H3;
255 skip = 3;
256 len -= 3;
257 while ((c = page->content[page->content_pos + skip]) &&
258 (c == ' ' || c == '\t')) {
259 skip++;
260 len--;
261 }
262 goto print_line;
263 }
264
265 if (page->content[page->content_pos] == '#' &&
266 page->content[page->content_pos + 1] == '#') {
267 /* ## h2 */
268 page->browser->style = STYLE_H2;
269 skip = 2;
270 len -= 2;
271 while ((c = page->content[page->content_pos + skip]) &&
272 (c == ' ' || c == '\t')) {
273 skip++;
274 len--;
275 }
276 goto print_line;
277 }
278
279 if (page->content[page->content_pos] == '#') {
280 /* # h1 */
281 page->browser->style = STYLE_H1;
282 skip = 1;
283 len--;
284 while ((c = page->content[page->content_pos + skip]) &&
285 (c == ' ' || c == '\t')) {
286 skip++;
287 len--;
288 }
289 goto print_line;
290 }
291
292 if (page->content[page->content_pos] == '*') {
293 /* * list item */
294 page->browser->style = STYLE_LIST;
295 skip = 1;
296 len--;
297 while ((c = page->content[page->content_pos + skip]) &&
298 (c == ' ' || c == '\t')) {
299 skip++;
300 len--;
301 }
302 browser_print(page->browser, " • ", 3, false);
303 goto print_line;
304 }
305
306 if (page->content[page->content_pos] == '>') {
307 /* > quote text */
308 page->browser->style = STYLE_QUOTE;
309 skip = 1;
310 len--;
311 while ((c = page->content[page->content_pos + skip]) &&
312 (c == ' ' || c == '\t')) {
313 skip++;
314 len--;
315 }
316 goto print_line;
317 }
318
319 print_line:
320 if (len) {
321 browser_print(page->browser,
322 page->content + page->content_pos + skip, len, newline);
323 } else if (newline) {
324 /* print blank lines in the default size */
325 page->browser->style = STYLE_NONE;
326 browser_print(page->browser, "\r", 1, false);
327 }
328
329 page->content_pos += skip + len + trail;
330 }
331
332 if (!PAGE_CAN_READ_MORE(page) && page->content_pos < page->content_len) {
333 browser_print(page->browser, page->content + page->content_pos,
334 page->content_len - page->content_pos, false);
335 page->content_pos = page->content_len;
336 }
337
338 return PAGE_CAN_READ_MORE(page);
339 }
340
341 void
342 gemini_reset(page_handle pageh)
343 {
344 struct page *page = *pageh;
345
346 /* restart at body */
347 page->parse_state = PARSE_STATE_BODY;
348 page->content_pos = page->header_len;
349 }
350
351 static bool
352 parse_header(struct page *page, char *str, size_t len)
353 {
354 char fail[32], *newuri, *encoded, *query;
355 Str255 txt;
356 GrafPtr win;
357 DialogPtr dlg;
358 Handle ihandle;
359 Rect irect, dlgrect;
360 size_t tlen;
361 short status, hit, itype, ret, top;
362
363 BROWSER_DEBUGF((page->browser, "Received header: %s", str));
364 if (!(str[0] >= '0' && str[0] <= '9' &&
365 str[1] >= '0' && str[1] <= '9' && str[2] == ' ')) {
366 browser_statusf(page->browser, "Error: Malformed response %s", str);
367 return false;
368 }
369
370 page->server_status = ((str[0] - '0') * 10) + (str[1] - '0');
371
372 memcpy(fail, str, MIN(len, sizeof(fail)));
373 fail[MIN(len, sizeof(fail) - 1)] = '\0';
374
375 if (page->server_status >= 10 && page->server_status <= 19) {
376 /* input requested */
377 GetPort(&win);
378
379 if ((dlg = GetNewDialog(INPUT_DLOG_ID, NULL,
380 (WindowPtr)-1)) == NULL) {
381 warn("Can't find DLOG %d", INPUT_DLOG_ID);
382 return false;
383 }
384 center_in_screen(dlg->portRect.right - dlg->portRect.left,
385 dlg->portRect.bottom - dlg->portRect.top, true, &dlgrect);
386 MoveWindow(dlg, dlgrect.left, dlgrect.top, true);
387
388 snprintf((char *)txt, sizeof(txt), "Input requested from %s",
389 page->uri->hostname);
390 CtoPstr(txt);
391 SetWTitle(dlg, txt);
392
393 GetDItem(dlg, INPUT_DLOG_PROMPT_ID, &itype, &ihandle, &irect);
394 tlen = len - 2;
395 if (tlen >= sizeof(txt))
396 tlen = sizeof(txt) - 1;
397 memcpy(txt + 1, str + 3, tlen);
398 txt[0] = tlen;
399 SetIText(ihandle, txt);
400
401 ShowWindow(dlg);
402 for (;;) {
403 ModalDialog(ModalDialogFilter, &hit);
404 if (hit == ok || hit == cancel)
405 break;
406 }
407
408 if (hit == ok) {
409 GetDItem(dlg, INPUT_DLOG_INPUT_ID, &itype, &ihandle, &irect);
410 GetIText(ihandle, txt);
411 PtoCstr(txt);
412 }
413
414 DisposDialog(dlg);
415
416 SetPort(win);
417
418 if (hit != ok) {
419 browser_statusf(page->browser, "Canceled input");
420 return false;
421 }
422
423 /* make a new uri with this same one but send input as query */
424 encoded = uri_encode(txt);
425 if (encoded == NULL) {
426 browser_statusf(page->browser, "Error: Out of memory");
427 return false;
428 }
429
430 tlen = 1 + strlen(encoded);
431 query = xmalloc(tlen + 1);
432 if (query == NULL) {
433 xfree(&encoded);
434 browser_statusf(page->browser, "Error: Out of memory");
435 }
436
437 snprintf(query, tlen + 1, "?%s", encoded);
438 xfree(&encoded);
439
440 page->redir_to = build_relative_uri(page->uri, query, tlen);
441 xfree(&query);
442 if (page->redir_to == NULL)
443 browser_statusf(page->browser, "Error: Out of memory");
444
445 return false;
446 }
447
448 if (page->server_status >= 20 && page->server_status <= 29) {
449 /* success */
450 if (len + 3 + 1 > sizeof(page->content_type))
451 len = sizeof(page->content_type) - 1;
452 memcpy(page->content_type, str + 3, len - 2);
453 page->content_type[len - 2] = '\0';
454 return true;
455 }
456
457 if (page->server_status >= 30 && page->server_status <= 39) {
458 /* redirect */
459 page->redir_to = build_relative_uri(page->uri, str + 3, len - 2);
460 /* TODO: infinite loop detection */
461 return false;
462 }
463
464 if (page->server_status >= 40 && page->server_status <= 49) {
465 /* temp fail */
466 browser_statusf(page->browser, "Error: Server reported temporary "
467 "failure: %s", fail);
468 return false;
469 }
470
471 if (page->server_status >= 50 && page->server_status <= 59) {
472 /* perm fail */
473 browser_statusf(page->browser, "Error: Server reported "
474 "permanent failure: %s", fail);
475 return false;
476 }
477
478 if (page->server_status >= 60 && page->server_status <= 69) {
479 /* auth, not supported */
480 browser_statusf(page->browser, "Error: Auth not supported (%d)",
481 status);
482 return false;
483 }
484
485 browser_statusf(page->browser, "Error: Unsupported status: %s", fail);
486 return false;
487 }