source: lib/http_client.c @ 3f808ca

Last change on this file since 3f808ca was 3f808ca, checked in by Wilmer van der Gaast <wilmer@…>, at 2011-12-11T16:38:02Z

Support HTTP/1.1 redirect status codes and use HTTPS for OAuth setup. This
is required for identi.ca and really should be done for Twitter as well.
Twitter OAuth is still broken though, it seems to disagree about signatures.

  • Property mode set to 100644
File size: 12.3 KB
Line 
1  /********************************************************************\
2  * BitlBee -- An IRC to other IM-networks gateway                     *
3  *                                                                    *
4  * Copyright 2002-2011 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., 59 Temple Place,
23  Suite 330, Boston, MA  02111-1307  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, 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, gpointer data )
41{
42        struct http_request *req;
43        int error = 0;
44       
45        req = g_new0( struct http_request, 1 );
46       
47        if( ssl )
48        {
49                req->ssl = ssl_connect( host, port, http_ssl_connected, req );
50                if( req->ssl == NULL )
51                        error = 1;
52        }
53        else
54        {
55                req->fd = proxy_connect( host, port, http_connected, req );
56                if( req->fd < 0 )
57                        error = 1;
58        }
59       
60        if( error )
61        {
62                http_free( req );
63                return NULL;
64        }
65       
66        req->func = func;
67        req->data = data;
68        req->request = g_strdup( request );
69        req->request_length = strlen( request );
70        req->redir_ttl = 3;
71       
72        if( getenv( "BITLBEE_DEBUG" ) )
73                printf( "About to send HTTP request:\n%s\n", req->request );
74       
75        return( req );
76}
77
78struct http_request *http_dorequest_url( char *url_string, http_input_function func, gpointer data )
79{
80        url_t *url = g_new0( url_t, 1 );
81        char *request;
82        void *ret;
83       
84        if( !url_set( url, url_string ) )
85        {
86                g_free( url );
87                return NULL;
88        }
89       
90        if( url->proto != PROTO_HTTP && url->proto != PROTO_HTTPS )
91        {
92                g_free( url );
93                return NULL;
94        }
95       
96        request = g_strdup_printf( "GET %s HTTP/1.0\r\n"
97                                   "Host: %s\r\n"
98                                   "Connection: close\r\n"
99                                   "User-Agent: BitlBee " BITLBEE_VERSION " " ARCH "/" CPU "\r\n"
100                                   "\r\n", url->file, url->host );
101       
102        ret = http_dorequest( url->host, url->port,
103                              url->proto == PROTO_HTTPS, request, func, data );
104       
105        g_free( url );
106        g_free( request );
107        return ret;
108}
109
110/* This one is actually pretty simple... Might get more calls if we can't write
111   the whole request at once. */
112static gboolean http_connected( gpointer data, int source, b_input_condition cond )
113{
114        struct http_request *req = data;
115        int st;
116       
117        if( source < 0 )
118                goto error;
119       
120        if( req->inpa > 0 )
121                b_event_remove( req->inpa );
122       
123        sock_make_nonblocking( req->fd );
124       
125        if( req->ssl )
126        {
127                st = ssl_write( req->ssl, req->request + req->bytes_written,
128                                req->request_length - req->bytes_written );
129                if( st < 0 )
130                {
131                        if( ssl_errno != SSL_AGAIN )
132                        {
133                                ssl_disconnect( req->ssl );
134                                goto error;
135                        }
136                }
137        }
138        else
139        {
140                st = write( source, req->request + req->bytes_written,
141                                    req->request_length - req->bytes_written );
142                if( st < 0 )
143                {
144                        if( !sockerr_again() )
145                        {
146                                closesocket( req->fd );
147                                goto error;
148                        }
149                }
150        }
151       
152        if( st > 0 )
153                req->bytes_written += st;
154       
155        if( req->bytes_written < req->request_length )
156                req->inpa = b_input_add( source,
157                                         req->ssl ? ssl_getdirection( req->ssl ) : B_EV_IO_WRITE,
158                                         http_connected, req );
159        else
160                req->inpa = b_input_add( source, B_EV_IO_READ, http_incoming_data, req );
161       
162        return FALSE;
163       
164error:
165        req->status_string = g_strdup( "Error while writing HTTP request" );
166       
167        req->func( req );
168        http_free( req );
169        return FALSE;
170}
171
172static gboolean http_ssl_connected( gpointer data, void *source, b_input_condition cond )
173{
174        struct http_request *req = data;
175       
176        if( source == NULL )
177                return http_connected( data, -1, cond );
178       
179        req->fd = ssl_getfd( source );
180       
181        return http_connected( data, req->fd, cond );
182}
183
184static gboolean http_incoming_data( gpointer data, int source, b_input_condition cond )
185{
186        struct http_request *req = data;
187        int evil_server = 0;
188        char buffer[2048];
189        char *end1, *end2;
190        int st;
191       
192        if( req->inpa > 0 )
193                b_event_remove( req->inpa );
194       
195        if( req->ssl )
196        {
197                st = ssl_read( req->ssl, buffer, sizeof( buffer ) );
198                if( st < 0 )
199                {
200                        if( ssl_errno != SSL_AGAIN )
201                        {
202                                /* goto cleanup; */
203                               
204                                /* YAY! We have to deal with crappy Microsoft
205                                   servers that LOVE to send invalid TLS
206                                   packets that abort connections! \o/ */
207                               
208                                goto got_reply;
209                        }
210                }
211                else if( st == 0 )
212                {
213                        goto got_reply;
214                }
215        }
216        else
217        {
218                st = read( req->fd, buffer, sizeof( buffer ) );
219                if( st < 0 )
220                {
221                        if( !sockerr_again() )
222                        {
223                                req->status_string = g_strdup( strerror( errno ) );
224                                goto cleanup;
225                        }
226                }
227                else if( st == 0 )
228                {
229                        goto got_reply;
230                }
231        }
232       
233        if( st > 0 )
234        {
235                req->reply_headers = g_realloc( req->reply_headers, req->bytes_read + st + 1 );
236                memcpy( req->reply_headers + req->bytes_read, buffer, st );
237                req->bytes_read += st;
238        }
239       
240        /* There will be more! */
241        req->inpa = b_input_add( req->fd,
242                                 req->ssl ? ssl_getdirection( req->ssl ) : B_EV_IO_READ,
243                                 http_incoming_data, req );
244       
245        if( ssl_pending( req->ssl ) )
246                return http_incoming_data( data, source, cond );
247        else
248                return FALSE;
249
250got_reply:
251        /* Maybe if the webserver is overloaded, or when there's bad SSL
252           support... */
253        if( req->bytes_read == 0 )
254        {
255                req->status_string = g_strdup( "Empty HTTP reply" );
256                goto cleanup;
257        }
258       
259        /* Zero termination is very convenient. */
260        req->reply_headers[req->bytes_read] = 0;
261       
262        /* Find the separation between headers and body, and keep stupid
263           webservers in mind. */
264        end1 = strstr( req->reply_headers, "\r\n\r\n" );
265        end2 = strstr( req->reply_headers, "\n\n" );
266       
267        if( end2 && end2 < end1 )
268        {
269                end1 = end2 + 1;
270                evil_server = 1;
271        }
272        else if( end1 )
273        {
274                end1 += 2;
275        }
276        else
277        {
278                req->status_string = g_strdup( "Malformed HTTP reply" );
279                goto cleanup;
280        }
281       
282        *end1 = 0;
283       
284        if( getenv( "BITLBEE_DEBUG" ) )
285                printf( "HTTP response headers:\n%s\n", req->reply_headers );
286       
287        if( evil_server )
288                req->reply_body = end1 + 1;
289        else
290                req->reply_body = end1 + 2;
291       
292        req->body_size = req->reply_headers + req->bytes_read - req->reply_body;
293       
294        if( ( end1 = strchr( req->reply_headers, ' ' ) ) != NULL )
295        {
296                if( sscanf( end1 + 1, "%d", &req->status_code ) != 1 )
297                {
298                        req->status_string = g_strdup( "Can't parse status code" );
299                        req->status_code = -1;
300                }
301                else
302                {
303                        char *eol;
304                       
305                        if( evil_server )
306                                eol = strchr( end1, '\n' );
307                        else
308                                eol = strchr( end1, '\r' );
309                       
310                        req->status_string = g_strndup( end1 + 1, eol - end1 - 1 );
311                       
312                        /* Just to be sure... */
313                        if( ( eol = strchr( req->status_string, '\r' ) ) )
314                                *eol = 0;
315                        if( ( eol = strchr( req->status_string, '\n' ) ) )
316                                *eol = 0;
317                }
318        }
319        else
320        {
321                req->status_string = g_strdup( "Can't locate status code" );
322                req->status_code = -1;
323        }
324       
325        if( ( ( req->status_code >= 301 && req->status_code <= 303 ) ||
326              req->status_code == 307 ) && req->redir_ttl-- > 0 )
327        {
328                char *loc, *new_request, *new_host;
329                int error = 0, new_port, new_proto;
330               
331                /* We might fill it again, so let's not leak any memory. */
332                g_free( req->status_string );
333                req->status_string = NULL;
334               
335                loc = strstr( req->reply_headers, "\nLocation: " );
336                if( loc == NULL ) /* We can't handle this redirect... */
337                {
338                        req->status_string = g_strdup( "Can't locate Location: header" );
339                        goto cleanup;
340                }
341               
342                loc += 11;
343                while( *loc == ' ' )
344                        loc ++;
345               
346                /* TODO/FIXME: Possibly have to handle relative redirections,
347                   and rewrite Host: headers. Not necessary for now, it's
348                   enough for passport authentication like this. */
349               
350                if( *loc == '/' )
351                {
352                        /* Just a different pathname... */
353                       
354                        /* Since we don't cache the servername, and since we
355                           don't need this yet anyway, I won't implement it. */
356                       
357                        req->status_string = g_strdup( "Can't handle recursive redirects" );
358                       
359                        goto cleanup;
360                }
361                else
362                {
363                        /* A whole URL */
364                        url_t *url;
365                        char *s;
366                        const char *new_method;
367                       
368                        s = strstr( loc, "\r\n" );
369                        if( s == NULL )
370                                goto cleanup;
371                       
372                        url = g_new0( url_t, 1 );
373                        *s = 0;
374                       
375                        if( !url_set( url, loc ) )
376                        {
377                                req->status_string = g_strdup( "Malformed redirect URL" );
378                                g_free( url );
379                                goto cleanup;
380                        }
381                       
382                        /* Find all headers and, if necessary, the POST request contents.
383                           Skip the old Host: header though. This crappy code here means
384                           anything using this http_client MUST put the Host: header at
385                           the top. */
386                        if( !( ( s = strstr( req->request, "\r\nHost: " ) ) &&
387                               ( s = strstr( s + strlen( "\r\nHost: " ), "\r\n" ) ) ) )
388                        {
389                                req->status_string = g_strdup( "Error while rebuilding request string" );
390                                g_free( url );
391                                goto cleanup;
392                        }
393                       
394                        /* More or less HTTP/1.0 compliant, from my reading of RFC 2616.
395                           Always perform a GET request unless we received a 301. 303 was
396                           meant for this but it's HTTP/1.1-only and we're specifically
397                           speaking HTTP/1.0. ...
398                           
399                           Well except someone at identi.ca's didn't bother reading any
400                           RFCs and just return HTTP/1.1-specific status codes to HTTP/1.0
401                           requests. Fuckers. So here we are, handle 301..303,307. */
402                        if( strncmp( req->request, "GET", 3 ) == 0 )
403                                /* GETs never become POSTs. */
404                                new_method = "GET";
405                        else if( req->status_code == 302 || req->status_code == 303 )
406                                /* 302 de-facto becomes GET, 303 as specified by RFC 2616#10.3.3 */
407                                new_method = "GET";
408                        else
409                                /* 301 de-facto should stay POST, 307 specifally RFC 2616#10.3.8 */
410                                new_method = "POST";
411                       
412                        /* Okay, this isn't fun! We have to rebuild the request... :-( */
413                        new_request = g_strdup_printf( "%s %s HTTP/1.0\r\nHost: %s%s",
414                                                       new_method, url->file, url->host, s );
415                       
416                        new_host = g_strdup( url->host );
417                        new_port = url->port;
418                        new_proto = url->proto;
419                       
420                        /* If we went from POST to GET, truncate the request content. */
421                        if( new_request[0] != req->request[0] && new_request[0] == 'G' &&
422                            ( s = strstr( new_request, "\r\n\r\n" ) ) )
423                                s[4] = '\0';
424                       
425                        g_free( url );
426                }
427               
428                if( req->ssl )
429                        ssl_disconnect( req->ssl );
430                else
431                        closesocket( req->fd );
432               
433                req->fd = -1;
434                req->ssl = NULL;
435               
436                if( getenv( "BITLBEE_DEBUG" ) )
437                        printf( "New headers for redirected HTTP request:\n%s\n", new_request );
438       
439                if( new_proto == PROTO_HTTPS )
440                {
441                        req->ssl = ssl_connect( new_host, new_port, http_ssl_connected, req );
442                        if( req->ssl == NULL )
443                                error = 1;
444                }
445                else
446                {
447                        req->fd = proxy_connect( new_host, new_port, http_connected, req );
448                        if( req->fd < 0 )
449                                error = 1;
450                }
451                g_free( new_host );
452               
453                if( error )
454                {
455                        req->status_string = g_strdup( "Connection problem during redirect" );
456                        g_free( new_request );
457                        goto cleanup;
458                }
459               
460                g_free( req->request );
461                g_free( req->reply_headers );
462                req->request = new_request;
463                req->request_length = strlen( new_request );
464                req->bytes_read = req->bytes_written = req->inpa = 0;
465                req->reply_headers = req->reply_body = NULL;
466               
467                return FALSE;
468        }
469       
470        /* Assume that a closed connection means we're finished, this indeed
471           breaks with keep-alive connections and faulty connections. */
472        req->finished = 1;
473
474cleanup:
475        if( req->ssl )
476                ssl_disconnect( req->ssl );
477        else
478                closesocket( req->fd );
479       
480        if( getenv( "BITLBEE_DEBUG" ) && req )
481                printf( "Finishing HTTP request with status: %s\n",
482                        req->status_string ? req->status_string : "NULL" );
483       
484        req->func( req );
485        http_free( req );
486        return FALSE;
487}
488
489static void http_free( struct http_request *req )
490{
491        g_free( req->request );
492        g_free( req->reply_headers );
493        g_free( req->status_string );
494        g_free( req );
495}
496
Note: See TracBrowser for help on using the repository browser.