summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt2
-rw-r--r--res/about/version.gmi1
-rw-r--r--src/gmrequest.c164
-rw-r--r--src/gopher.c206
-rw-r--r--src/gopher.h45
5 files changed, 269 insertions, 149 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index cb97185c..0d311682 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -103,6 +103,8 @@ set (SOURCES
103 src/gmrequest.h 103 src/gmrequest.h
104 src/gmutil.c 104 src/gmutil.c
105 src/gmutil.h 105 src/gmutil.h
106 src/gopher.c
107 src/gopher.h
106 src/history.c 108 src/history.c
107 src/history.h 109 src/history.h
108 src/lookup.c 110 src/lookup.c
diff --git a/res/about/version.gmi b/res/about/version.gmi
index 12c00b3b..d73fdf25 100644
--- a/res/about/version.gmi
+++ b/res/about/version.gmi
@@ -7,6 +7,7 @@
7# Release notes 7# Release notes
8 8
9## 0.8 9## 0.8
10* Added support for Gopher.
10* Added support for the full palette of 8-bit ANSI foreground colors. 11* Added support for the full palette of 8-bit ANSI foreground colors.
11* Added option to disable smooth scrolling. 12* Added option to disable smooth scrolling.
12* Added keybindings for Back/Forward navigation. 13* Added keybindings for Back/Forward navigation.
diff --git a/src/gmrequest.c b/src/gmrequest.c
index 8a9226a1..d143e8da 100644
--- a/src/gmrequest.c
+++ b/src/gmrequest.c
@@ -23,6 +23,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
23#include "gmrequest.h" 23#include "gmrequest.h"
24#include "gmutil.h" 24#include "gmutil.h"
25#include "gmcerts.h" 25#include "gmcerts.h"
26#include "gopher.h"
26#include "app.h" /* dataDir_App() */ 27#include "app.h" /* dataDir_App() */
27#include "embedded.h" 28#include "embedded.h"
28#include "ui/text.h" 29#include "ui/text.h"
@@ -121,10 +122,7 @@ struct Impl_GmRequest {
121 enum iGmRequestState state; 122 enum iGmRequestState state;
122 iString url; 123 iString url;
123 iTlsRequest * req; 124 iTlsRequest * req;
124 iSocket * gopher; /* socket for Gopher connections */ 125 iGopher gopher;
125 char gopherType;
126 iBlock gopherBody;
127 iBool gopherPre;
128 iGmResponse resp; 126 iGmResponse resp;
129 iAudience * updated; 127 iAudience * updated;
130 iAudience * finished; 128 iAudience * finished;
@@ -341,127 +339,13 @@ static const iBlock *replaceVariables_(const iBlock *block) {
341 return block; 339 return block;
342} 340}
343 341
344iLocalDef iBool isLineTerminator_(const char *str) {
345 return str[0] == '\r' && str[1] == '\n';
346}
347
348iLocalDef iBool isPreformatted_(iRangecc text) {
349 int numPunct = 0;
350 iBool isSpace = iFalse;
351 for (const char *ch = text.start; ch != text.end; ch++) {
352 if (ispunct(*ch)) {
353 if (++numPunct == 4)
354 return iTrue;
355 }
356 else {
357 numPunct = 0;
358 }
359 if (*ch == ' ' || *ch == '\n') {
360 if (isSpace) return iTrue;
361 isSpace = iTrue;
362 }
363 else {
364 isSpace = iFalse;
365 }
366 }
367 return iFalse;
368}
369
370static void setGopherPre_GmRequest_(iGmRequest *d, iBool pre) {
371 if (pre && !d->gopherPre) {
372 appendCStr_Block(&d->resp.body, "```\n");
373 }
374 else if (!pre && d->gopherPre) {
375 appendCStr_Block(&d->resp.body, "```\n");
376 }
377 d->gopherPre = pre;
378}
379
380static iBool convertGopherLines_GmRequest_(iGmRequest *d) {
381 iBool converted = iFalse;
382 iRangecc body = range_Block(&d->gopherBody);
383 iRegExp *pattern = new_RegExp("(.)([^\t]*)\t([^\t]*)\t([^\t]*)\t([0-9]+)", caseInsensitive_RegExpOption);
384 for (;;) {
385 /* Find the end of the line. */
386 iRangecc line = { body.start, body.start };
387 while (line.end < body.end - 1 && !isLineTerminator_(line.end)) {
388 line.end++;
389 }
390 if (line.end >= body.end - 1 || !isLineTerminator_(line.end)) {
391 /* Not a complete line. */
392 break;
393 }
394 body.start = line.end + 2;
395 iRegExpMatch m;
396 init_RegExpMatch(&m);
397 if (matchRange_RegExp(pattern, line, &m)) {
398 const char lineType = *capturedRange_RegExpMatch(&m, 1).start;
399 const iRangecc text = capturedRange_RegExpMatch(&m, 2);
400 const iRangecc path = capturedRange_RegExpMatch(&m, 3);
401 const iRangecc domain = capturedRange_RegExpMatch(&m, 4);
402 const iRangecc port = capturedRange_RegExpMatch(&m, 5);
403 iString *out = new_String();
404 switch (lineType) {
405 case 'i':
406 case '3': {
407 setGopherPre_GmRequest_(d, isPreformatted_(text));
408 appendData_Block(&d->resp.body, text.start, size_Range(&text));
409 appendCStr_Block(&d->resp.body, "\n");
410 break;
411 }
412 case '0':
413 case '1':
414 case '7':
415 case '4':
416 case '5':
417 case '9':
418 case 'g':
419 case 'I':
420 case 's': {
421 iBeginCollect();
422 setGopherPre_GmRequest_(d, iFalse);
423 format_String(out,
424 "=> gopher://%s:%s/%c%s %s\n",
425 cstr_Rangecc(domain),
426 cstr_Rangecc(port),
427 lineType,
428 cstr_Rangecc(path),
429 cstr_Rangecc(text));
430 appendData_Block(&d->resp.body, constBegin_String(out), size_String(out));
431 iEndCollect();
432 break;
433 }
434 default:
435 break; /* Ignore unknown types. */
436 }
437 delete_String(out);
438 }
439 }
440 iRelease(pattern);
441 remove_Block(&d->gopherBody, 0, body.start - constBegin_Block(&d->gopherBody));
442 return converted;
443}
444
445static void gopherRead_GmRequest_(iGmRequest *d, iSocket *socket) { 342static void gopherRead_GmRequest_(iGmRequest *d, iSocket *socket) {
446 iBool notifyUpdate = iFalse; 343 iBool notifyUpdate = iFalse;
447 lock_Mutex(&d->mutex); 344 lock_Mutex(&d->mutex);
448 d->resp.statusCode = success_GmStatusCode; 345 d->resp.statusCode = success_GmStatusCode;
449 iBlock *data = readAll_Socket(socket); 346 iBlock *data = readAll_Socket(socket);
450 if (!isEmpty_Block(data)) { 347 if (!isEmpty_Block(data)) {
451 if (d->gopherType == '1') { 348 processResponse_Gopher(&d->gopher, data);
452 setCStr_String(&d->resp.meta, "text/gemini");
453 append_Block(&d->gopherBody, data);
454 if (convertGopherLines_GmRequest_(d)) {
455 notifyUpdate = iTrue;
456 }
457 }
458 else {
459 if (d->gopherType == '0') {
460 setCStr_String(&d->resp.meta, "text/plain");
461 }
462 append_Block(&d->resp.body, data);
463 notifyUpdate = iTrue;
464 }
465 } 349 }
466 delete_Block(data); 350 delete_Block(data);
467 unlock_Mutex(&d->mutex); 351 unlock_Mutex(&d->mutex);
@@ -499,28 +383,15 @@ static void beginGopherConnection_GmRequest_(iGmRequest *d, const iString *host,
499 if (port == 0) { 383 if (port == 0) {
500 port = 70; /* default port */ 384 port = 70; /* default port */
501 } 385 }
502 clear_Block(&d->gopherBody); 386 clear_Block(&d->gopher.source);
503 d->state = receivingBody_GmRequestState; 387 d->gopher.meta = &d->resp.meta;
504 d->gopher = new_Socket(cstr_String(host), port); 388 d->gopher.output = &d->resp.body;
505 iConnect(Socket, d->gopher, readyRead, d, gopherRead_GmRequest_); 389 d->state = receivingBody_GmRequestState;
506 iConnect(Socket, d->gopher, disconnected, d, gopherDisconnected_GmRequest_); 390 d->gopher.socket = new_Socket(cstr_String(host), port);
507 iConnect(Socket, d->gopher, error, d, gopherError_GmRequest_); 391 iConnect(Socket, d->gopher.socket, readyRead, d, gopherRead_GmRequest_);
508 open_Socket(d->gopher); 392 iConnect(Socket, d->gopher.socket, disconnected, d, gopherDisconnected_GmRequest_);
509 iUrl parts; 393 iConnect(Socket, d->gopher.socket, error, d, gopherError_GmRequest_);
510 init_Url(&parts, &d->url); 394 open_Gopher(&d->gopher, &d->url);
511 d->gopherType = '1';
512 if (!isEmpty_Range(&parts.path)) {
513 if (*parts.path.start == '/') {
514 parts.path.start++;
515 }
516 if (parts.path.start < parts.path.end) {
517 d->gopherType = *parts.path.start;
518 parts.path.start++;
519 }
520 }
521 d->gopherPre = iFalse;
522 writeData_Socket(d->gopher, parts.path.start, size_Range(&parts.path));
523 writeData_Socket(d->gopher, "\r\n", 2);
524} 395}
525 396
526/*----------------------------------------------------------------------------------------------*/ 397/*----------------------------------------------------------------------------------------------*/
@@ -529,11 +400,9 @@ void init_GmRequest(iGmRequest *d, iGmCerts *certs) {
529 init_Mutex(&d->mutex); 400 init_Mutex(&d->mutex);
530 init_GmResponse(&d->resp); 401 init_GmResponse(&d->resp);
531 init_String(&d->url); 402 init_String(&d->url);
532 init_Block(&d->gopherBody, 0); 403 init_Gopher(&d->gopher);
533 d->certs = certs; 404 d->certs = certs;
534 d->req = NULL; 405 d->req = NULL;
535 d->gopher = NULL;
536 d->gopherType = 0;
537 d->updated = NULL; 406 d->updated = NULL;
538 d->finished = NULL; 407 d->finished = NULL;
539 d->state = initialized_GmRequestState; 408 d->state = initialized_GmRequestState;
@@ -554,8 +423,7 @@ void deinit_GmRequest(iGmRequest *d) {
554 unlock_Mutex(&d->mutex); 423 unlock_Mutex(&d->mutex);
555 } 424 }
556 iReleasePtr(&d->req); 425 iReleasePtr(&d->req);
557 iReleasePtr(&d->gopher); 426 deinit_Gopher(&d->gopher);
558 deinit_Block(&d->gopherBody);
559 delete_Audience(d->finished); 427 delete_Audience(d->finished);
560 delete_Audience(d->updated); 428 delete_Audience(d->updated);
561 deinit_GmResponse(&d->resp); 429 deinit_GmResponse(&d->resp);
@@ -719,9 +587,7 @@ void cancel_GmRequest(iGmRequest *d) {
719 if (d->req) { 587 if (d->req) {
720 cancel_TlsRequest(d->req); 588 cancel_TlsRequest(d->req);
721 } 589 }
722 if (d->gopher) { 590 cancel_Gopher(&d->gopher);
723 close_Socket(d->gopher);
724 }
725} 591}
726 592
727iBool isFinished_GmRequest(const iGmRequest *d) { 593iBool isFinished_GmRequest(const iGmRequest *d) {
diff --git a/src/gopher.c b/src/gopher.c
new file mode 100644
index 00000000..9a503442
--- /dev/null
+++ b/src/gopher.c
@@ -0,0 +1,206 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
23#include "gopher.h"
24
25#include <ctype.h>
26
27iDefineTypeConstruction(Gopher)
28
29iLocalDef iBool isLineTerminator_(const char *str) {
30 return str[0] == '\r' && str[1] == '\n';
31}
32
33static iBool isPreformatted_(iRangecc text) {
34 int numPunct = 0;
35 int numSpace = 0;
36 for (const char *ch = text.start; ch != text.end; ch++) {
37 if (*ch > 32 && ispunct(*ch)) {
38 if (++numPunct == 4)
39 return iTrue;
40 }
41 else {
42 numPunct = 0;
43 }
44 if (*ch == ' ' || *ch == '\n') {
45 if (++numSpace == 3) return iTrue;
46 }
47 else {
48 numSpace = 0;
49 }
50 }
51 return iFalse;
52}
53
54static void setPre_Gopher_(iGopher *d, iBool pre) {
55 if (pre && !d->isPre) {
56 appendCStr_Block(d->output, "```\n");
57 }
58 else if (!pre && d->isPre) {
59 appendCStr_Block(d->output, "```\n");
60 }
61 d->isPre = pre;
62}
63
64static iBool convertSource_Gopher_(iGopher *d) {
65 iBool converted = iFalse;
66 iRangecc body = range_Block(&d->source);
67 iRegExp *pattern = new_RegExp("(.)([^\t]*)\t([^\t]*)\t([^\t]*)\t([0-9]+)",
68 caseInsensitive_RegExpOption);
69 for (;;) {
70 /* Find the end of the line. */
71 iRangecc line = { body.start, body.start };
72 while (line.end < body.end - 1 && !isLineTerminator_(line.end)) {
73 line.end++;
74 }
75 if (line.end >= body.end - 1 || !isLineTerminator_(line.end)) {
76 /* Not a complete line. */
77 break;
78 }
79 body.start = line.end + 2;
80 iRegExpMatch m;
81 init_RegExpMatch(&m);
82 if (matchRange_RegExp(pattern, line, &m)) {
83 const char lineType = *capturedRange_RegExpMatch(&m, 1).start;
84 const iRangecc text = capturedRange_RegExpMatch(&m, 2);
85 const iRangecc path = capturedRange_RegExpMatch(&m, 3);
86 const iRangecc domain = capturedRange_RegExpMatch(&m, 4);
87 const iRangecc port = capturedRange_RegExpMatch(&m, 5);
88 iString *buf = new_String();
89 switch (lineType) {
90 case 'i':
91 case '3': {
92 setPre_Gopher_(d, isPreformatted_(text));
93 appendData_Block(d->output, text.start, size_Range(&text));
94 appendCStr_Block(d->output, "\n");
95 break;
96 }
97 case '0':
98 case '1':
99 case '7':
100 case '4':
101 case '5':
102 case '9':
103 case 'g':
104 case 'I':
105 case 's': {
106 iBeginCollect();
107 setPre_Gopher_(d, iFalse);
108 format_String(buf,
109 "=> gopher://%s:%s/%c%s %s\n",
110 cstr_Rangecc(domain),
111 cstr_Rangecc(port),
112 lineType,
113 cstr_Rangecc(path),
114 cstr_Rangecc(text));
115 appendData_Block(d->output, constBegin_String(buf), size_String(buf));
116 iEndCollect();
117 break;
118 }
119 default:
120 break; /* Ignore unknown types. */
121 }
122 delete_String(buf);
123 }
124 }
125 iRelease(pattern);
126 remove_Block(&d->source, 0, body.start - constBegin_Block(&d->source));
127 return converted;
128}
129
130void init_Gopher(iGopher *d) {
131 d->socket = NULL;
132 d->type = 0;
133 init_Block(&d->source, 0);
134 d->isPre = iFalse;
135 d->meta = NULL;
136 d->output = NULL;
137}
138
139void deinit_Gopher(iGopher *d) {
140 deinit_Block(&d->source);
141 iReleasePtr(&d->socket);
142}
143
144void open_Gopher(iGopher *d, const iString *url) {
145 open_Socket(d->socket);
146 iUrl parts;
147 init_Url(&parts, url);
148 d->type = '1';
149 if (!isEmpty_Range(&parts.path)) {
150 if (*parts.path.start == '/') {
151 parts.path.start++;
152 }
153 if (parts.path.start < parts.path.end) {
154 d->type = *parts.path.start;
155 parts.path.start++;
156 }
157 }
158 /* MIME type determined by the URI. */
159 switch (d->type) {
160 case '0':
161 setCStr_String(d->meta, "text/plain");
162 break;
163 case '1':
164 setCStr_String(d->meta, "text/gemini");
165 break;
166 case '4':
167 setCStr_String(d->meta, "application/mac-binhex");
168 break;
169 case 'g':
170 setCStr_String(d->meta, "image/gif");
171 break;
172 case 'I':
173 setCStr_String(d->meta, "image/generic");
174 break;
175 case 's':
176 setCStr_String(d->meta, "audio/wave");
177 break;
178 default:
179 setCStr_String(d->meta, "application/octet-stream");
180 break;
181 }
182 d->isPre = iFalse;
183 writeData_Socket(d->socket, parts.path.start, size_Range(&parts.path));
184 writeData_Socket(d->socket, "\r\n", 2);
185}
186
187void cancel_Gopher(iGopher *d) {
188 if (d->socket) {
189 close_Socket(d->socket);
190 }
191}
192
193iBool processResponse_Gopher(iGopher *d, const iBlock *data) {
194 iBool changed = iFalse;
195 if (d->type == '1') {
196 append_Block(&d->source, data);
197 if (convertSource_Gopher_(d)) {
198 changed = iTrue;
199 }
200 }
201 else {
202 append_Block(d->output, data);
203 changed = iTrue;
204 }
205 return changed;
206}
diff --git a/src/gopher.h b/src/gopher.h
new file mode 100644
index 00000000..c085267b
--- /dev/null
+++ b/src/gopher.h
@@ -0,0 +1,45 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
23#pragma once
24
25#include "gmutil.h"
26
27#include <the_Foundation/regexp.h>
28#include <the_Foundation/socket.h>
29
30iDeclareType(Gopher)
31
32struct Impl_Gopher {
33 iSocket *socket;
34 char type;
35 iBlock source;
36 iBool isPre;
37 iString *meta;
38 iBlock * output;
39};
40
41iDeclareTypeConstruction(Gopher)
42
43void open_Gopher (iGopher *, const iString *url);
44iBool processResponse_Gopher (iGopher *, const iBlock *data);
45void cancel_Gopher (iGopher *);