source: lib/http_client.c @ 82149f4

Last change on this file since 82149f4 was 93d4d8f, checked in by GitHub <noreply@…>, at 2023-02-28T00:09:28Z

Handle HTTP 308 the same as 307 (#173)

This makes the HTTP client code handle status code 308 (Permanent
Redirect) in a similar manner to the already existing support for 307
(Temporary redirect). Like 307, no change of method is applied to the
redirect.

The use case for this has arisen from the bitlbee-mastodon plugin, the
Mastodon API returns 308 for some requests.

Co-authored-by: Ralf Ertzinger <ralf.ertzinger@…>

  • Property mode set to 100644
File size: 17.5 KB
Line 
1/********************************************************************\
2  * BitlBee -- An IRC to other IM-networks gateway                     *
3  *                                                                    *
4  * Copyright 2002-2013 Wilmer van der Gaast and others                *
5  \********************************************************************/
6
7/* HTTP(S) module                                                       */
8
9/*
10  This program is free software; you can redistribute it and/or modify
11  it under the terms of the GNU General Public License as published by
12  the Free Software Foundation; either version 2 of the License, or
13  (at your option) any later version.
14
15  This program is distributed in the hope that it will be useful,
16  but WITHOUT ANY WARRANTY; without even the implied warranty of
17  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  GNU General Public License for more details.
19
20  You should have received a copy of the GNU General Public License with
21  the Debian GNU/Linux distribution in /usr/share/common-licenses/GPL;
22  if not, write to the Free Software Foundation, Inc., 51 Franklin St.,
23  Fifth Floor, Boston, MA  02110-1301  USA
24*/
25
26#include <string.h>
27#include <stdio.h>
28
29#include "http_client.h"
30#include "url.h"
31#include "sock.h"
32
33
34static gboolean http_connected(gpointer data, int source, b_input_condition cond);
35static gboolean http_ssl_connected(gpointer data, int returncode, void *source, b_input_condition cond);
36static gboolean http_incoming_data(gpointer data, int source, b_input_condition cond);
37static void http_free(struct http_request *req);
38
39
40struct http_request *http_dorequest(char *host, int port, int ssl, char *request, http_input_function func,
41                                    gpointer data)
42{
43        struct http_request *req;
44        int error = 0;
45
46        req = g_new0(struct http_request, 1);
47
48        if (ssl) {
49                req->ssl = ssl_connect(host, port, TRUE, http_ssl_connected, req);
50                if (req->ssl == NULL) {
51                        error = 1;
52                }
53        } else {
54                req->fd = proxy_connect(host, port, http_connected, req);
55                if (req->fd < 0) {
56                        error = 1;
57                }
58        }
59
60        if (error) {
61                http_free(req);
62                return NULL;
63        }
64
65        req->func = func;
66        req->data = data;
67        req->request = g_strdup(request);
68        req->request_length = strlen(request);
69        req->redir_ttl = 3;
70        req->content_length = -1;
71
72        if (getenv("BITLBEE_DEBUG")) {
73                printf("About to send HTTP request:\n%s\n", req->request);
74        }
75
76        return req;
77}
78
79struct http_request *http_dorequest_url(char *url_string, http_input_function func, gpointer data)
80{
81        url_t *url = g_new0(url_t, 1);
82        char *request;
83        void *ret;
84
85        if (!url_set(url, url_string)) {
86                g_free(url);
87                return NULL;
88        }
89
90        if (url->proto != PROTO_HTTP && url->proto != PROTO_HTTPS) {
91                g_free(url);
92                return NULL;
93        }
94
95        request = g_strdup_printf("GET %s HTTP/1.0\r\n"
96                                  "Host: %s\r\n"
97                                  "User-Agent: BitlBee " BITLBEE_VERSION "\r\n"
98                                  "\r\n", url->file, url->host);
99
100        ret = http_dorequest(url->host, url->port,
101                             url->proto == PROTO_HTTPS, request, func, data);
102
103        g_free(url);
104        g_free(request);
105        return ret;
106}
107
108/* This one is actually pretty simple... Might get more calls if we can't write
109   the whole request at once. */
110static gboolean http_connected(gpointer data, int source, b_input_condition cond)
111{
112        struct http_request *req = data;
113        int st;
114
115        if (source < 0) {
116                goto error;
117        }
118
119        if (req->inpa > 0) {
120                b_event_remove(req->inpa);
121        }
122
123        sock_make_nonblocking(req->fd);
124
125        if (req->ssl) {
126                st = ssl_write(req->ssl, req->request + req->bytes_written,
127                               req->request_length - req->bytes_written);
128                if (st < 0) {
129                        if (ssl_errno != SSL_AGAIN) {
130                                ssl_disconnect(req->ssl);
131                                goto error;
132                        }
133                }
134        } else {
135                st = write(source, req->request + req->bytes_written,
136                           req->request_length - req->bytes_written);
137                if (st < 0) {
138                        if (!sockerr_again()) {
139                                closesocket(req->fd);
140                                goto error;
141                        }
142                }
143        }
144
145        if (st > 0) {
146                req->bytes_written += st;
147        }
148
149        if (req->bytes_written < req->request_length) {
150                req->inpa = b_input_add(source,
151                                        req->ssl ? ssl_getdirection(req->ssl) : B_EV_IO_WRITE,
152                                        http_connected, req);
153        } else {
154                req->inpa = b_input_add(source, B_EV_IO_READ, http_incoming_data, req);
155        }
156
157        return FALSE;
158
159error:
160        if (req->status_string == NULL) {
161                req->status_string = g_strdup("Error while writing HTTP request");
162        }
163
164        if (req->func != NULL) {
165                req->func(req);
166        }
167        http_free(req);
168        return FALSE;
169}
170
171static gboolean http_ssl_connected(gpointer data, int returncode, void *source, b_input_condition cond)
172{
173        struct http_request *req = data;
174
175        if (source == NULL) {
176                if (returncode != 0) {
177                        char *err = ssl_verify_strerror(returncode);
178                        req->status_string = g_strdup_printf(
179                                "Certificate verification problem 0x%x: %s",
180                                returncode, err ? err : "Unknown");
181                        g_free(err);
182                }
183                return http_connected(data, -1, cond);
184        }
185
186        req->fd = ssl_getfd(source);
187
188        return http_connected(data, req->fd, cond);
189}
190
191typedef enum {
192        CR_OK,
193        CR_EOF,
194        CR_ERROR,
195        CR_ABORT,
196} http_ret_t;
197
198static gboolean http_handle_headers(struct http_request *req);
199static http_ret_t http_process_chunked_data(struct http_request *req, const char *buffer, int len);
200static http_ret_t http_process_data(struct http_request *req, const char *buffer, int len);
201
202static gboolean http_incoming_data(gpointer data, int source, b_input_condition cond)
203{
204        struct http_request *req = data;
205        char buffer[4096];
206        int st;
207
208        if (req->inpa > 0) {
209                b_event_remove(req->inpa);
210                req->inpa = 0;
211        }
212
213        if (req->ssl) {
214                st = ssl_read(req->ssl, buffer, sizeof(buffer));
215                if (st < 0) {
216                        if (ssl_errno != SSL_AGAIN) {
217                                /* goto cleanup; */
218
219                                /* YAY! We have to deal with crappy Microsoft
220                                   servers that LOVE to send invalid TLS
221                                   packets that abort connections! \o/ */
222
223                                goto eof;
224                        }
225                } else if (st == 0) {
226                        goto eof;
227                }
228        } else {
229                st = read(req->fd, buffer, sizeof(buffer));
230                if (st < 0) {
231                        if (!sockerr_again()) {
232                                req->status_string = g_strdup(strerror(errno));
233                                goto cleanup;
234                        }
235                } else if (st == 0) {
236                        goto eof;
237                }
238        }
239
240        if (st > 0) {
241                http_ret_t c;
242
243                if (req->flags & HTTPC_CHUNKED) {
244                        c = http_process_chunked_data(req, buffer, st);
245                } else {
246                        c = http_process_data(req, buffer, st);
247                }
248
249                if (c == CR_EOF) {
250                        goto eof;
251                } else if (c == CR_ERROR || c == CR_ABORT) {
252                        return FALSE;
253                }
254        }
255
256        if (req->content_length != -1 &&
257            req->body_size >= req->content_length) {
258                goto eof;
259        }
260
261        if (ssl_pending(req->ssl)) {
262                return http_incoming_data(data, source, cond);
263        }
264
265        /* There will be more! */
266        req->inpa = b_input_add(req->fd,
267                                req->ssl ? ssl_getdirection(req->ssl) : B_EV_IO_READ,
268                                http_incoming_data, req);
269
270        return FALSE;
271
272eof:
273        req->flags |= HTTPC_EOF;
274
275        /* Maybe if the webserver is overloaded, or when there's bad SSL
276           support... */
277        if (req->bytes_read == 0) {
278                req->status_string = g_strdup("Empty HTTP reply");
279                goto cleanup;
280        }
281
282cleanup:
283        /* Avoid g_source_remove warnings */
284        req->inpa = 0;
285
286        if (req->ssl) {
287                ssl_disconnect(req->ssl);
288        } else {
289                closesocket(req->fd);
290        }
291
292        if (req->body_size < req->content_length) {
293                req->status_code = -1;
294                g_free(req->status_string);
295                req->status_string = g_strdup("Response truncated");
296        }
297
298        if (getenv("BITLBEE_DEBUG") && req) {
299                printf("Finishing HTTP request with status: %s\n",
300                       req->status_string ? req->status_string : "NULL");
301        }
302
303        if (req->func != NULL) {
304                req->func(req);
305        }
306        http_free(req);
307        return FALSE;
308}
309
310static http_ret_t http_process_chunked_data(struct http_request *req, const char *buffer, int len)
311{
312        char *chunk, *eos, *s;
313
314        if (len < 0) {
315                return TRUE;
316        }
317
318        if (len > 0) {
319                req->cbuf = g_realloc(req->cbuf, req->cblen + len + 1);
320                memcpy(req->cbuf + req->cblen, buffer, len);
321                req->cblen += len;
322                req->cbuf[req->cblen] = '\0';
323        }
324
325        /* Turns out writing a proper chunked-encoding state machine is not
326           that simple. :-( I've tested this one feeding it byte by byte so
327           I hope it's solid now. */
328        chunk = req->cbuf;
329        eos = req->cbuf + req->cblen;
330        while (TRUE) {
331                int clen = 0;
332
333                /* Might be a \r\n from the last chunk. */
334                s = chunk;
335                while (g_ascii_isspace(*s)) {
336                        s++;
337                }
338                /* Chunk length. Might be incomplete. */
339                if (s < eos && sscanf(s, "%x", &clen) != 1) {
340                        return CR_ERROR;
341                }
342                while (g_ascii_isxdigit(*s)) {
343                        s++;
344                }
345
346                /* If we read anything here, it *must* be \r\n. */
347                if (strncmp(s, "\r\n", MIN(2, eos - s)) != 0) {
348                        return CR_ERROR;
349                }
350                s += 2;
351
352                if (s >= eos) {
353                        break;
354                }
355
356                /* 0-length chunk means end of response. */
357                if (clen == 0) {
358                        return CR_EOF;
359                }
360
361                /* Wait for the whole chunk to arrive. */
362                if (s + clen > eos) {
363                        break;
364                }
365                if (http_process_data(req, s, clen) != CR_OK) {
366                        return CR_ABORT;
367                }
368
369                chunk = s + clen;
370        }
371
372        if (chunk != req->cbuf) {
373                req->cblen = eos - chunk;
374                s = g_memdup(chunk, req->cblen + 1);
375                g_free(req->cbuf);
376                req->cbuf = s;
377        }
378
379        return CR_OK;
380}
381
382static http_ret_t http_process_data(struct http_request *req, const char *buffer, int len)
383{
384        if (len <= 0) {
385                return CR_OK;
386        }
387
388        if (!req->reply_body) {
389                req->reply_headers = g_realloc(req->reply_headers, req->bytes_read + len + 1);
390                memcpy(req->reply_headers + req->bytes_read, buffer, len);
391                req->bytes_read += len;
392                req->reply_headers[req->bytes_read] = '\0';
393
394                if (strstr(req->reply_headers, "\r\n\r\n") ||
395                    strstr(req->reply_headers, "\n\n")) {
396                        /* We've now received all headers. Look for something
397                           interesting. */
398                        if (!http_handle_headers(req)) {
399                                return CR_ABORT;
400                        }
401
402                        /* Start parsing the body as chunked if required. */
403                        if (req->flags & HTTPC_CHUNKED) {
404                                return http_process_chunked_data(req, NULL, 0);
405                        }
406                }
407        } else {
408                int pos = req->reply_body - req->sbuf;
409                req->sbuf = g_realloc(req->sbuf, req->sblen + len + 1);
410                memcpy(req->sbuf + req->sblen, buffer, len);
411                req->bytes_read += len;
412                req->sblen += len;
413                req->sbuf[req->sblen] = '\0';
414                req->reply_body = req->sbuf + pos;
415                req->body_size = req->sblen - pos;
416        }
417
418        if ((req->flags & HTTPC_STREAMING) && req->reply_body && req->func != NULL) {
419                req->func(req);
420        }
421
422        return CR_OK;
423}
424
425/* Splits headers and body. Checks result code, in case of 300s it'll handle
426   redirects. If this returns FALSE, don't call any callbacks! */
427static gboolean http_handle_headers(struct http_request *req)
428{
429        char *end1, *end2, *s;
430        int evil_server = 0;
431
432        /* Zero termination is very convenient. */
433        req->reply_headers[req->bytes_read] = '\0';
434
435        /* Find the separation between headers and body, and keep stupid
436           webservers in mind. */
437        end1 = strstr(req->reply_headers, "\r\n\r\n");
438        end2 = strstr(req->reply_headers, "\n\n");
439
440        if (end2 && end2 < end1) {
441                end1 = end2 + 1;
442                evil_server = 1;
443        } else if (end1) {
444                end1 += 2;
445        } else {
446                req->status_string = g_strdup("Malformed HTTP reply");
447                return TRUE;
448        }
449
450        *end1 = '\0';
451
452        if (getenv("BITLBEE_DEBUG")) {
453                printf("HTTP response headers:\n%s\n", req->reply_headers);
454        }
455
456        if (evil_server) {
457                req->reply_body = end1 + 1;
458        } else {
459                req->reply_body = end1 + 2;
460        }
461
462        /* Separately allocated space for headers and body. */
463        req->sblen = req->body_size = req->reply_headers + req->bytes_read - req->reply_body;
464        req->sbuf = req->reply_body = g_memdup(req->reply_body, req->body_size + 1);
465        req->reply_headers = g_realloc(req->reply_headers, end1 - req->reply_headers + 1);
466
467        if ((end1 = strchr(req->reply_headers, ' ')) != NULL) {
468                if (sscanf(end1 + 1, "%hd", &req->status_code) != 1) {
469                        req->status_string = g_strdup("Can't parse status code");
470                        req->status_code = -1;
471                } else {
472                        char *eol;
473
474                        if (evil_server) {
475                                eol = strchr(end1, '\n');
476                        } else {
477                                eol = strchr(end1, '\r');
478                        }
479
480                        req->status_string = g_strndup(end1 + 1, eol - end1 - 1);
481
482                        /* Just to be sure... */
483                        if ((eol = strchr(req->status_string, '\r'))) {
484                                *eol = 0;
485                        }
486                        if ((eol = strchr(req->status_string, '\n'))) {
487                                *eol = 0;
488                        }
489                }
490        } else {
491                req->status_string = g_strdup("Can't locate status code");
492                req->status_code = -1;
493        }
494
495        if (((req->status_code >= 301 && req->status_code <= 303) ||
496             req->status_code == 307 || req->status_code == 308) && req->redir_ttl-- > 0) {
497                char *loc, *new_request, *new_host;
498                int error = 0, new_port, new_proto;
499
500                /* We might fill it again, so let's not leak any memory. */
501                g_free(req->status_string);
502                req->status_string = NULL;
503
504                loc = strstr(req->reply_headers, "\nLocation: ");
505                if (loc == NULL) { /* We can't handle this redirect... */
506                        req->status_string = g_strdup("Can't locate Location: header");
507                        return TRUE;
508                }
509
510                loc += 11;
511                while (*loc == ' ') {
512                        loc++;
513                }
514
515                /* TODO/FIXME: Possibly have to handle relative redirections,
516                   and rewrite Host: headers. Not necessary for now, it's
517                   enough for passport authentication like this. */
518
519                if (*loc == '/') {
520                        /* Just a different pathname... */
521
522                        /* Since we don't cache the servername, and since we
523                           don't need this yet anyway, I won't implement it. */
524
525                        req->status_string = g_strdup("Can't handle relative redirects");
526
527                        return TRUE;
528                } else {
529                        /* A whole URL */
530                        url_t *url;
531                        char *s, *version, *headers;
532                        const char *new_method;
533
534                        s = strstr(loc, "\r\n");
535                        if (s == NULL) {
536                                return TRUE;
537                        }
538
539                        url = g_new0(url_t, 1);
540                        *s = 0;
541
542                        if (!url_set(url, loc)) {
543                                req->status_string = g_strdup("Malformed redirect URL");
544                                g_free(url);
545                                return TRUE;
546                        }
547
548                        /* Find all headers and, if necessary, the POST request contents.
549                           Skip the old Host: header though. This crappy code here means
550                           anything using this http_client MUST put the Host: header at
551                           the top. */
552                        if (!((s = strstr(req->request, "\r\nHost: ")) &&
553                              (s = strstr(s + strlen("\r\nHost: "), "\r\n")))) {
554                                req->status_string = g_strdup("Error while rebuilding request string");
555                                g_free(url);
556                                return TRUE;
557                        }
558                        headers = s;
559
560                        /* More or less HTTP/1.0 compliant, from my reading of RFC 2616.
561                           Always perform a GET request unless we received a 301. 303 was
562                           meant for this but it's HTTP/1.1-only and we're specifically
563                           speaking HTTP/1.0. ...
564
565                           Well except someone at identi.ca's didn't bother reading any
566                           RFCs and just return HTTP/1.1-specific status codes to HTTP/1.0
567                           requests. Fuckers. So here we are, handle 301..303,307,308. */
568                        if (strncmp(req->request, "GET", 3) == 0) {
569                                /* GETs never become POSTs. */
570                                new_method = "GET";
571                        } else if (req->status_code == 302 || req->status_code == 303) {
572                                /* 302 de-facto becomes GET, 303 as specified by RFC 2616#10.3.3 */
573                                new_method = "GET";
574                        } else {
575                                /* 301 de-facto should stay POST, 307/308 specifally RFC 2616#10.3.8 */
576                                new_method = "POST";
577                        }
578
579                        if ((version = strstr(req->request, " HTTP/")) &&
580                            (s = strstr(version, "\r\n"))) {
581                                version++;
582                                version = g_strndup(version, s - version);
583                        } else {
584                                version = g_strdup("HTTP/1.0");
585                        }
586
587                        /* Okay, this isn't fun! We have to rebuild the request... :-( */
588                        new_request = g_strdup_printf("%s %s %s\r\nHost: %s%s",
589                                                      new_method, url->file, version,
590                                                      url->host, headers);
591
592                        new_host = g_strdup(url->host);
593                        new_port = url->port;
594                        new_proto = url->proto;
595
596                        /* If we went from POST to GET, truncate the request content. */
597                        if (new_request[0] != req->request[0] && new_request[0] == 'G' &&
598                            (s = strstr(new_request, "\r\n\r\n"))) {
599                                s[4] = '\0';
600                        }
601
602                        g_free(url);
603                        g_free(version);
604                }
605
606                if (req->ssl) {
607                        ssl_disconnect(req->ssl);
608                } else {
609                        closesocket(req->fd);
610                }
611
612                req->fd = -1;
613                req->ssl = NULL;
614
615                if (getenv("BITLBEE_DEBUG")) {
616                        printf("New headers for redirected HTTP request:\n%s\n", new_request);
617                }
618
619                if (new_proto == PROTO_HTTPS) {
620                        req->ssl = ssl_connect(new_host, new_port, TRUE, http_ssl_connected, req);
621                        if (req->ssl == NULL) {
622                                error = 1;
623                        }
624                } else {
625                        req->fd = proxy_connect(new_host, new_port, http_connected, req);
626                        if (req->fd < 0) {
627                                error = 1;
628                        }
629                }
630                g_free(new_host);
631
632                if (error) {
633                        req->status_string = g_strdup("Connection problem during redirect");
634                        g_free(new_request);
635                        return TRUE;
636                }
637
638                g_free(req->request);
639                g_free(req->reply_headers);
640                g_free(req->sbuf);
641                req->request = new_request;
642                req->request_length = strlen(new_request);
643                req->bytes_read = req->bytes_written = req->inpa = 0;
644                req->reply_headers = req->reply_body = NULL;
645                req->sbuf = req->cbuf = NULL;
646                req->sblen = req->cblen = 0;
647
648                return FALSE;
649        }
650
651        if ((s = get_rfc822_header(req->reply_headers, "Content-Length", 0)) &&
652            sscanf(s, "%d", &req->content_length) != 1) {
653                req->content_length = -1;
654        }
655        g_free(s);
656
657        if ((s = get_rfc822_header(req->reply_headers, "Transfer-Encoding", 0))) {
658                if (strcasestr(s, "chunked")) {
659                        req->flags |= HTTPC_CHUNKED;
660                        req->cbuf = req->sbuf;
661                        req->cblen = req->sblen;
662
663                        req->reply_body = req->sbuf = g_strdup("");
664                        req->body_size = req->sblen = 0;
665                }
666                g_free(s);
667        }
668
669        return TRUE;
670}
671
672void http_flush_bytes(struct http_request *req, size_t len)
673{
674        if (len <= 0 || len > req->body_size || !(req->flags & HTTPC_STREAMING)) {
675                return;
676        }
677
678        req->reply_body += len;
679        req->body_size -= len;
680
681        if (req->reply_body - req->sbuf >= 512) {
682                char *new = g_memdup(req->reply_body, req->body_size + 1);
683                g_free(req->sbuf);
684                req->reply_body = req->sbuf = new;
685                req->sblen = req->body_size;
686        }
687}
688
689void http_close(struct http_request *req)
690{
691        if (!req) {
692                return;
693        }
694
695        if (req->inpa > 0) {
696                b_event_remove(req->inpa);
697        }
698
699        if (req->ssl) {
700                ssl_disconnect(req->ssl);
701        } else {
702                proxy_disconnect(req->fd);
703        }
704
705        http_free(req);
706}
707
708static void http_free(struct http_request *req)
709{
710        g_free(req->request);
711        g_free(req->reply_headers);
712        g_free(req->status_string);
713        g_free(req->sbuf);
714        g_free(req->cbuf);
715        g_free(req);
716}
Note: See TracBrowser for help on using the repository browser.