small weather widget for X11
1/*
2 * Copyright (c) 2020-2022 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 <err.h>
18#include <stdio.h>
19#include <stdlib.h>
20#include <string.h>
21#include <unistd.h>
22#include <sys/select.h>
23#include "http.h"
24
25extern char *__progname;
26
27struct url *
28url_parse(const char *str)
29{
30 struct url *url = NULL;
31 char *scheme, *host, *path;
32 unsigned int port;
33 int ret, pos;
34 size_t len, schemelen, hostlen, pathlen;
35
36 len = strlen(str);
37 scheme = malloc(len + 1);
38 if (scheme == NULL)
39 err(1, "malloc");
40 host = malloc(len + 1);
41 if (host == NULL)
42 err(1, "malloc");
43 path = malloc(len + 1);
44 if (path == NULL)
45 err(1, "malloc");
46
47 /* scheme://host:port/path */
48 ret = sscanf(str, "%[^:]://%[^:]:%u%s%n", scheme, host, &port, path,
49 &pos);
50 if (ret == 4) {
51 if (pos > len)
52 errx(1, "url_parse sscanf overflow");
53 goto consolidate;
54 }
55
56 /* scheme://host/path */
57 ret = sscanf(str, "%[^:]://%[^/]%s%n", scheme, host, path, &pos);
58 if (ret == 3) {
59 if (pos > len)
60 errx(1, "url_parse sscanf overflow");
61 if (strcmp(scheme, "http") == 0)
62 port = 80;
63 else if (strcmp(scheme, "https") == 0)
64 port = 443;
65 else
66 goto cleanup;
67 goto consolidate;
68 }
69
70 goto cleanup;
71
72consolidate:
73 schemelen = strlen(scheme);
74 hostlen = strlen(host);
75 pathlen = strlen(path);
76
77 /*
78 * Put everything in a single chunk of memory so the caller can just
79 * free(url)
80 */
81 len = sizeof(struct url) + schemelen + 1 + hostlen + 1 + pathlen + 1;
82 url = malloc(len);
83 if (url == NULL)
84 err(1, "malloc");
85
86 url->scheme = (char *)url + sizeof(struct url);
87 len = strlcpy(url->scheme, scheme, schemelen + 1);
88
89 url->host = url->scheme + len + 1;
90 len = strlcpy(url->host, host, hostlen + 1);
91
92 url->path = url->host + len + 1;
93 len = strlcpy(url->path, path, pathlen + 1);
94
95 url->port = port;
96
97cleanup:
98 free(scheme);
99 free(host);
100 free(path);
101
102 return url;
103}
104
105char *
106url_encode(unsigned char *str)
107{
108 char *ret = NULL;
109 size_t len, n;
110
111encode:
112 for (n = 0, len = 0; str[n] != '\0'; n++) {
113 if ((str[n] >= 'A' && str[n] <= 'Z') ||
114 (str[n] >= 'a' && str[n] <= 'z') ||
115 (str[n] >= '0' && str[n] <= '9') ||
116 (str[n] == '-' || str[n] == '_' || str[n] == '.' ||
117 str[n] == '~')) {
118 if (ret)
119 ret[len] = str[n];
120 len++;
121 } else {
122 if (ret)
123 snprintf(ret + len, 4, "%%%02X", str[n]);
124 len += 3;
125 }
126 }
127
128 if (ret) {
129 ret[len] = '\0';
130 return ret;
131 }
132
133 ret = malloc(len + 1);
134 if (ret == NULL)
135 err(1, "malloc");
136 len = 0;
137 goto encode;
138}
139
140struct http_request *
141http_get(const char *surl)
142{
143 struct url *url;
144 struct http_request *req;
145 struct hostent *he;
146 struct sockaddr_in addr;
147 size_t len, tlen;
148 char ip_s[16];
149#if TLS
150 struct tls_config *tls_config;
151 int tret;
152#endif
153
154 url = url_parse(surl);
155 if (url == NULL)
156 return NULL;
157
158 req = malloc(sizeof(struct http_request));
159 if (req == NULL)
160 err(1, "malloc");
161 memset(req, 0, sizeof(struct http_request));
162 req->url = url;
163
164 if (strcmp(url->scheme, "https") == 0) {
165#if TLS
166 req->https = 1;
167#else
168 errx(1, "requested HTTPS URL but no TLS support: %s", surl);
169#endif
170 }
171
172 he = gethostbyname(req->url->host);
173 if (he == NULL) {
174 warnx("couldn't resolve host %s: %s", req->url->host,
175 hstrerror(h_errno));
176 goto error;
177 }
178
179 req->socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
180 if (req->socket == -1)
181 err(1, "socket");
182
183 memset(&addr, 0, sizeof(addr));
184 addr.sin_family = AF_INET;
185 addr.sin_port = htons(url->port);
186 addr.sin_addr = *((struct in_addr *)he->h_addr);
187
188 inet_ntop(AF_INET, &addr.sin_addr, ip_s, sizeof(ip_s));
189
190#if DEBUG
191 printf("connecting to %s (%s) %sto fetch %s\n",
192 req->url->host, ip_s, req->https ? "(with TLS) " : "",
193 req->url->path);
194#endif
195
196 if (connect(req->socket, (struct sockaddr *)&addr,
197 sizeof(addr)) == -1) {
198 warn("failed connecting to %s (%s) port %d",
199 req->url->host, ip_s, req->url->port);
200 goto error;
201 }
202
203#if TLS
204 if (req->https) {
205 tls_config = tls_config_new();
206 if (tls_config == NULL)
207 errx(1, "tls_config allocation failed");
208 if (tls_config_set_protocols(tls_config,
209 TLS_PROTOCOLS_ALL) != 0)
210 errx(1, "tls set protocols failed: %s",
211 tls_config_error(tls_config));
212 if (tls_config_set_ciphers(tls_config, "legacy") != 0)
213 errx(1, "tls set ciphers failed: %s",
214 tls_config_error(tls_config));
215
216 req->tls = tls_client();
217 if (req->tls == NULL)
218 errx(1, "tls_client allocation failed");
219
220 if (tls_configure(req->tls, tls_config) != 0)
221 errx(1, "tls_configure failed: %s",
222 tls_config_error(tls_config));
223
224 if (tls_connect_socket(req->tls, req->socket,
225 req->url->host) != 0) {
226 warnx("TLS connect to %s failed: %s", req->url->host,
227 tls_error(req->tls));
228 goto error;
229 }
230
231 do {
232 tret = tls_handshake(req->tls);
233 } while (tret == TLS_WANT_POLLIN || tret == TLS_WANT_POLLOUT);
234
235 if (tret != 0) {
236 warnx("TLS handshake to %s failed: %s",
237 req->url->host, tls_error(req->tls));
238 goto error;
239 }
240 }
241#endif
242
243 tlen = 256 + strlen(req->url->host) + strlen(req->url->path);
244 req->message = malloc(tlen);
245 if (req->message == NULL)
246 err(1, "malloc");
247
248 len = snprintf(req->message, tlen,
249 "GET %s HTTP/1.0\r\n"
250 "Host: %s\r\n"
251 "User-Agent: %s\r\n"
252 "Accept: */*\r\n"
253 "\r\n",
254 req->url->path, req->url->host, __progname);
255 if (len > tlen)
256 errx(1, "snprintf overflow");
257
258#if DEBUG
259 printf(">>>[%zu] %s\n", len, req->message);
260#endif
261
262#if TLS
263 if (req->https) {
264 do {
265 tlen = tls_write(req->tls, req->message, len);
266 } while (tlen == TLS_WANT_POLLIN || tlen == TLS_WANT_POLLOUT);
267 } else
268#endif
269 {
270 tlen = write(req->socket, req->message, len);
271 }
272 if (tlen != len)
273 err(1, "short write");
274
275 return req;
276
277error:
278 http_req_free(req);
279 return NULL;
280}
281
282ssize_t
283http_req_read(struct http_request *req, char *data, size_t len)
284{
285 fd_set fds;
286 struct timeval timeout;
287 ssize_t ret;
288
289 if (!req || !req->socket)
290 return -1;
291
292 FD_ZERO(&fds);
293 FD_SET(req->socket, &fds);
294 timeout.tv_sec = 0;
295 timeout.tv_usec = 0;
296
297 switch (select(req->socket + 1, &fds, NULL, NULL, &timeout)) {
298 case -1:
299 err(1, "select");
300 case 0:
301 return 0;
302 }
303
304#if TLS
305 if (req->https) {
306 do {
307 ret = tls_read(req->tls, data, len);
308 } while (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT);
309 } else
310#endif
311 {
312 ret = read(req->socket, data, len);
313 }
314#if DEBUG
315 printf("<<<[%zu] %s\n", len, data);
316#endif
317
318 if (ret == -1) {
319#if TLS
320 if (req->https) {
321 tls_close(req->tls);
322 tls_free(req->tls);
323 req->tls = NULL;
324 req->https = 0;
325 }
326#endif
327 close(req->socket);
328 req->socket = 0;
329 return -1;
330 }
331 return ret;
332}
333
334int
335http_req_skip_header(struct http_request *req)
336{
337 size_t len, n;
338
339 for (;;) {
340 if (req->chunk_len > 3) {
341 /*
342 * Leave last 3 bytes of previous read in case \r\n\r\n
343 * happens across reads.
344 */
345 memmove(req->chunk, req->chunk + req->chunk_len - 3,
346 req->chunk_len - 3);
347 req->chunk_len = 3;
348 }
349 len = http_req_read(req, req->chunk + req->chunk_len,
350 sizeof(req->chunk) - req->chunk_len);
351 if (len < 0)
352 return 0;
353 if (len == 0)
354 continue;
355 req->chunk_len += len;
356
357 for (n = 3; n < req->chunk_len; n++) {
358 if (req->chunk[n - 3] != '\r' ||
359 req->chunk[n - 2] != '\n' ||
360 req->chunk[n - 1] != '\r' ||
361 req->chunk[n] != '\n')
362 continue;
363
364 req->chunk_len -= n + 1;
365 memmove(req->chunk, req->chunk + n + 1, req->chunk_len);
366 req->chunk_off = 0;
367 return 1;
368 }
369 }
370
371 return 0;
372}
373
374char *
375http_req_chunk_peek(struct http_request *req)
376{
377 if (req->chunk_len == 0 || (req->chunk_off + 1 > req->chunk_len)) {
378 req->chunk_len = http_req_read(req, req->chunk,
379 sizeof(req->chunk));
380 if (req->chunk_len < 0)
381 return NULL;
382 req->chunk_off = 0;
383 }
384
385 if (req->chunk_len == 0 || (req->chunk_off + 1 > req->chunk_len))
386 return NULL;
387
388 return req->chunk + req->chunk_off;
389}
390
391char *
392http_req_chunk_read(struct http_request *req)
393{
394 char *chunk;
395
396 chunk = http_req_chunk_peek(req);
397 if (chunk == NULL)
398 return NULL;
399
400 req->chunk_off = req->chunk_len;
401 return chunk;
402}
403
404char
405http_req_byte_peek(struct http_request *req)
406{
407 if (req->chunk_len == 0 || (req->chunk_off + 1 > req->chunk_len)) {
408 req->chunk_len = http_req_read(req, req->chunk,
409 sizeof(req->chunk));
410 if (req->chunk_len < 0)
411 return 0;
412 req->chunk_off = 0;
413 }
414
415 if (req->chunk_len == 0 || (req->chunk_off + 1 > req->chunk_len))
416 return 0;
417
418 return req->chunk[req->chunk_off];
419}
420
421char
422http_req_byte_read(struct http_request *req)
423{
424 char c;
425
426 c = http_req_byte_peek(req);
427 if (c == 0)
428 return 0;
429
430 req->chunk_off++;
431 return c;
432}
433
434void
435http_req_free(struct http_request *req)
436{
437 if (req == NULL)
438 return;
439
440#if TLS
441 if (req->https && req->tls) {
442 tls_close(req->tls);
443 tls_free(req->tls);
444 req->tls = NULL;
445 }
446#endif
447 if (req->socket)
448 close(req->socket);
449 if (req->message != NULL)
450 free(req->message);
451 if (req->url)
452 free(req->url);
453}