Blob


1 /*
2 * Copyright (c) 2021-2023 Omar Polo <op@omarpolo.com>
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 */
17 #include "gmid.h"
19 #include <sys/socket.h>
21 #include <assert.h>
22 #include <ctype.h>
23 #include <errno.h>
24 #include <locale.h>
25 #include <string.h>
26 #include <wchar.h>
28 enum debug {
29 DEBUG_NONE,
30 DEBUG_CODE,
31 DEBUG_HEADER,
32 DEBUG_META,
33 DEBUG_ALL,
34 };
36 /* flags */
37 int debug;
38 int dont_verify_name;
39 int flag2;
40 int flag3;
41 int nop;
42 int redirects = 5;
43 int timer;
44 int quiet;
45 const char *cert;
46 const char *key;
47 const char *proxy_host;
48 const char *proxy_port;
49 const char *sni;
51 /* state */
52 struct tls_config *tls_conf;
54 static void
55 timeout(int signo)
56 {
57 dprintf(2, "%s: timer expired\n", getprogname());
58 exit(1);
59 }
61 static void
62 load_tls_conf(void)
63 {
64 if ((tls_conf = tls_config_new()) == NULL)
65 err(1, "tls_config_new");
67 tls_config_insecure_noverifycert(tls_conf);
68 if (dont_verify_name)
69 tls_config_insecure_noverifyname(tls_conf);
71 if (flag2 &&
72 tls_config_set_protocols(tls_conf, TLS_PROTOCOL_TLSv1_2) == -1)
73 errx(1, "can't set TLSv1.2");
74 if (flag3 &&
75 tls_config_set_protocols(tls_conf, TLS_PROTOCOL_TLSv1_3) == -1)
76 errx(1, "can't set TLSv1.3");
78 if (cert != NULL &&
79 tls_config_set_keypair_file(tls_conf, cert, key) == -1)
80 errx(1, "can't load client certificate %s", cert);
81 }
83 static void
84 connectto(struct tls *ctx, const char *host, const char *port)
85 {
86 struct addrinfo hints, *res, *res0;
87 int error;
88 int saved_errno;
89 int s;
90 const char *cause = NULL;
91 const char *sname;
93 if (proxy_host != NULL) {
94 host = proxy_host;
95 port = proxy_port;
96 }
98 if ((sname = sni) == NULL)
99 sname = host;
101 memset(&hints, 0, sizeof(hints));
102 hints.ai_family = AF_UNSPEC;
103 hints.ai_socktype = SOCK_STREAM;
104 error = getaddrinfo(host, port, &hints, &res0);
105 if (error)
106 errx(1, "%s", gai_strerror(error));
108 s = -1;
109 for (res = res0; res != NULL; res = res->ai_next) {
110 s = socket(res->ai_family, res->ai_socktype,
111 res->ai_protocol);
112 if (s == -1) {
113 cause = "socket";
114 continue;
117 if (connect(s, res->ai_addr, res->ai_addrlen) == -1) {
118 cause = "connect";
119 saved_errno = errno;
120 close(s);
121 errno = saved_errno;
122 s = -1;
123 continue;
126 break;
129 if (s == -1)
130 err(1, "%s: can't connect to %s:%s", cause,
131 host, port);
133 freeaddrinfo(res0);
135 if (tls_connect_socket(ctx, s, sname) == -1)
136 errx(1, "tls_connect_socket: %s", tls_error(ctx));
139 static void
140 doreq(struct tls *ctx, const char *buf)
142 size_t s;
143 ssize_t w;
145 s = strlen(buf);
146 while (s != 0) {
147 switch (w = tls_write(ctx, buf, s)) {
148 case 0:
149 case -1:
150 errx(1, "tls_write: %s", tls_error(ctx));
151 case TLS_WANT_POLLIN:
152 case TLS_WANT_POLLOUT:
153 continue;
156 s -= w;
157 buf += w;
161 static size_t
162 dorep(struct tls *ctx, uint8_t *buf, size_t len)
164 ssize_t w;
165 size_t tot = 0;
167 while (len != 0) {
168 switch (w = tls_read(ctx, buf, len)) {
169 case 0:
170 return tot;
171 case -1:
172 errx(1, "tls_write: %s", tls_error(ctx));
173 case TLS_WANT_POLLIN:
174 case TLS_WANT_POLLOUT:
175 continue;
178 len -= w;
179 buf += w;
180 tot += w;
183 return tot;
186 static void
187 safeprint(FILE *fp, const char *str)
189 int len;
190 wchar_t wc;
192 for (; *str != '\0'; str += len) {
193 if ((len = mbtowc(&wc, str, MB_CUR_MAX)) == -1) {
194 mbtowc(NULL, NULL, MB_CUR_MAX);
195 fputc('?', fp);
196 len = 1;
197 } else if (wcwidth(wc) == -1) {
198 fputc('?', fp);
199 } else if (wc != L'\n')
200 putwc(wc, fp);
203 fputc('\n', fp);
206 static int
207 get(const char *r)
209 struct tls *ctx;
210 struct iri iri;
211 int foundhdr = 0, code = -1, od;
212 char iribuf[GEMINI_URL_LEN];
213 char req[GEMINI_URL_LEN];
214 uint8_t buf[2048];
215 const char *parse_err, *host, *port;
216 int ret;
218 if (strlcpy(iribuf, r, sizeof(iribuf)) >= sizeof(iribuf))
219 errx(1, "iri too long: %s", r);
221 ret = snprintf(req, sizeof(req), "%s\r\n", r);
222 if (ret < 0 || (size_t)ret >= sizeof(req))
223 errx(1, "iri too long: %s", r);
225 if (!parse_iri(iribuf, &iri, &parse_err))
226 errx(1, "invalid IRI: %s", parse_err);
228 if (nop)
229 errx(0, "IRI OK");
231 if ((ctx = tls_client()) == NULL)
232 errx(1, "can't create tls context");
234 if (tls_configure(ctx, tls_conf) == -1)
235 errx(1, "tls_configure: %s", tls_error(ctx));
237 host = iri.host;
238 port = "1965";
239 if (*iri.port != '\0')
240 port = iri.port;
242 connectto(ctx, host, port);
244 od = 0;
245 while (!od) {
246 switch (tls_handshake(ctx)) {
247 case 0:
248 od = 1;
249 break;
250 case -1:
251 errx(1, "handshake: %s", tls_error(ctx));
255 doreq(ctx, req);
257 for (;;) {
258 uint8_t *t;
259 size_t len;
261 len = dorep(ctx, buf, sizeof(buf));
262 if (len == 0)
263 break;
265 if (foundhdr) {
266 write(1, buf, len);
267 continue;
269 foundhdr = 1;
271 if (memmem(buf, len, "\r\n", 2) == NULL)
272 errx(1, "invalid reply: no \\r\\n");
273 if (!isdigit((unsigned char)buf[0]) ||
274 !isdigit((unsigned char)buf[1]) ||
275 buf[2] != ' ')
276 errx(1, "invalid reply: invalid response format");
278 code = (buf[0] - '0') * 10 + buf[1] - '0';
280 if (debug == DEBUG_CODE) {
281 printf("%d\n", code);
282 break;
285 if (debug == DEBUG_HEADER) {
286 t = memmem(buf, len, "\r\n", 2);
287 assert(t != NULL);
288 *t = '\0';
289 printf("%s\n", buf);
290 break;
293 if (debug == DEBUG_META) {
294 t = memmem(buf, len, "\r\n", 2);
295 assert(t != NULL);
296 *t = '\0';
297 printf("%s\n", buf+3);
298 break;
301 if (debug == DEBUG_ALL) {
302 write(1, buf, len);
303 continue;
306 /* skip the header */
307 t = memmem(buf, len, "\r\n", 2);
308 assert(t != NULL);
309 if (code < 20 || code >= 30) {
310 *t = '\0';
311 if (!quiet) {
312 fprintf(stderr, "Server says: ");
313 /* skip return code */
314 safeprint(stderr, buf + 3);
317 t += 2; /* skip \r\n */
318 len -= t - buf;
319 write(1, t, len);
322 for (;;) {
323 switch (tls_close(ctx)) {
324 case TLS_WANT_POLLIN:
325 case TLS_WANT_POLLOUT:
326 continue;
327 case -1:
328 warnx("tls_close: %s", tls_error(ctx));
329 /* fallthrough */
330 default:
331 tls_free(ctx);
332 return code;
337 static void __attribute__((noreturn))
338 usage(void)
340 fprintf(stderr, "version: " GG_STRING "\n");
341 fprintf(stderr, "usage: %s [-23Nnq] [-C cert] [-d mode] [-H sni] "
342 "[-K key] [-P host[:port]]\n",
343 getprogname());
344 fprintf(stderr, " [-T seconds] gemini://...\n");
345 exit(1);
348 static int
349 parse_debug(const char *arg)
351 if (!strcmp(arg, "none"))
352 return DEBUG_NONE;
353 if (!strcmp(arg, "code"))
354 return DEBUG_CODE;
355 if (!strcmp(arg, "header"))
356 return DEBUG_HEADER;
357 if (!strcmp(arg, "meta"))
358 return DEBUG_META;
359 if (!strcmp(arg, "all"))
360 return DEBUG_ALL;
361 usage();
364 static void
365 parse_proxy(const char *arg)
367 char *at;
369 if ((proxy_host = strdup(arg)) == NULL)
370 err(1, "strdup");
372 proxy_port = "1965";
374 if ((at = strchr(proxy_host, ':')) == NULL)
375 return;
376 *at = '\0';
377 proxy_port = ++at;
379 if (strchr(proxy_port, ':') != NULL)
380 errx(1, "invalid port %s", proxy_port);
383 int
384 main(int argc, char **argv)
386 int ch, code;
387 const char *errstr;
389 setlocale(LC_CTYPE, "");
391 while ((ch = getopt(argc, argv, "23C:d:H:K:nNP:qT:")) != -1) {
392 switch (ch) {
393 case '2':
394 flag2 = 1;
395 break;
396 case '3':
397 flag3 = 1;
398 break;
399 case 'C':
400 cert = optarg;
401 break;
402 case 'd':
403 debug = parse_debug(optarg);
404 break;
405 case 'H':
406 sni = optarg;
407 break;
408 case 'K':
409 key = optarg;
410 break;
411 case 'N':
412 dont_verify_name = 1;
413 break;
414 case 'n':
415 nop = 1;
416 break;
417 case 'P':
418 parse_proxy(optarg);
419 dont_verify_name = 1;
420 break;
421 case 'q':
422 quiet = 1;
423 break;
424 case 'T':
425 timer = strtonum(optarg, 1, 1000, &errstr);
426 if (errstr != NULL)
427 errx(1, "timeout is %s: %s",
428 errstr, optarg);
429 signal(SIGALRM, timeout);
430 alarm(timer);
431 break;
432 default:
433 usage();
436 argc -= optind;
437 argv += optind;
439 if (flag2 + flag3 > 1) {
440 warnx("only -2 or -3 can be specified at the same time");
441 usage();
444 if ((cert != NULL && key == NULL) ||
445 (cert == NULL && key != NULL)) {
446 warnx("cert or key is missing");
447 usage();
450 if (argc != 1)
451 usage();
453 load_tls_conf();
455 signal(SIGPIPE, SIG_IGN);
457 #ifdef __OpenBSD__
458 if (pledge("stdio inet dns", NULL) == -1)
459 err(1, "pledge");
460 #endif
462 code = get(*argv);
463 if (code >= 20 && code < 30)
464 return 0;
465 return code;