transmission/libtransmission/http.c

899 lines
22 KiB
C

/******************************************************************************
* $Id$
*
* Copyright (c) 2006-2007 Transmission authors and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*****************************************************************************/
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdarg.h>
#include "transmission.h"
#include "http.h"
#include "net.h"
#include "trcompat.h"
#define HTTP_PORT 80 /* default http port 80 */
#define HTTP_TIMEOUT 60000 /* one minute http timeout */
#define HTTP_BUFSIZE 1500 /* 1.5K buffer size increment */
#define LF "\012"
#define CR "\015"
#define SP( cc ) ( ' ' == (cc) || '\t' == (cc) )
#define NL( cc ) ( '\015' == (cc) || '\012' == (cc) )
#define NUM( cc ) ( '0' <= (cc) && '9' >= (cc) )
#define ALEN( aa ) ( (int)(sizeof( (aa) ) / sizeof( (aa)[0] ) ) )
#define SKIP( off, len, done ) \
while( (off) < (len) && (done) ) { (off)++; };
static const char *
slice( const char * data, int * len, const char * delim );
static tr_tristate_t
sendrequest( tr_http_t * http );
static tr_tristate_t
receiveresponse( tr_http_t * http );
static int
checklength( tr_http_t * http );
static int
learnlength( tr_http_t * http );
#define EXPANDBUF( bs ) &(bs).buf, &(bs).used, &(bs).size
struct buf {
char * buf;
int size;
int used;
};
struct tr_http_s {
#define HTTP_STATE_CONSTRUCT 1
#define HTTP_STATE_RESOLVE 2
#define HTTP_STATE_CONNECT 3
#define HTTP_STATE_RECEIVE 4
#define HTTP_STATE_DONE 5
#define HTTP_STATE_ERROR 6
char state;
#define HTTP_LENGTH_UNKNOWN 1
#define HTTP_LENGTH_EOF 2
#define HTTP_LENGTH_FIXED 3
#define HTTP_LENGTH_CHUNKED 4
char lengthtype;
tr_resolve_t * resolve;
char * host;
int port;
int sock;
struct buf header;
struct buf body;
uint64_t date;
/*
eof: unused
fixed: lenptr is the start of the body, lenint is the body length
chunked: lenptr is start of chunk (after length), lenint is chunk size
*/
int chunkoff;
int chunklen;
};
int
tr_httpRequestType( const char * data, int len, char ** method, char ** uri )
{
const char * words[6];
int ii, ret;
const char * end;
/* find the end of the line */
for( end = data; data + len > end; end++ )
{
if( NL( *data) )
{
break;
}
}
/* find the first three "words" in the line */
for( ii = 0; ALEN( words ) > ii && data < end; ii++ )
{
/* find the next space or non-space */
while( data < end && ( ii % 2 ? !SP( *data ) : SP( *data ) ) )
{
data++;
}
/* save the beginning of the word */
words[ii] = data;
}
/* check for missing words */
if( ALEN( words) > ii )
{
return -1;
}
/* parse HTTP version */
ret = -1;
if( 8 <= words[5] - words[4] )
{
if( 0 == tr_strncasecmp( words[4], "HTTP/1.1", 8 ) )
{
ret = 11;
}
else if( 0 == tr_strncasecmp( words[4], "HTTP/1.0", 8 ) )
{
ret = 10;
}
}
/* copy the method */
if( 0 <= ret && NULL != method )
{
*method = tr_strndup( words[0], words[1] - words[0] );
if( NULL == *method )
{
ret = -1;
}
}
/* copy uri */
if( 0 <= ret && NULL != uri )
{
*uri = tr_strndup( words[2], words[3] - words[2] );
if( NULL == *uri )
{
free( *method );
ret = -1;
}
}
return ret;
}
int
tr_httpResponseCode( const char * data, int len )
{
char code[4];
int ret;
/* check for the minimum legal length */
if( 12 > len ||
/* check for valid http version */
0 != tr_strncasecmp( data, "HTTP/1.", 7 ) ||
( '1' != data[7] && '0' != data[7] ) ||
/* there should be at least one space after the version */
!SP( data[8] ) )
{
return -1;
}
/* skip any extra spaces */
data += 9;
len -= 9;
while( 0 < len && SP( *data ) )
{
data++;
len--;
}
/* check for a valid three-digit code */
if( 3 > len || !NUM( data[0] ) || !NUM( data[1] ) || !NUM( data[2] ) ||
( 3 < len && NUM( data[3] ) ) )
{
return -1;
}
/* parse and return the code */
memcpy( code, data, 3 );
code[3] = '\0';
ret = strtol( code, NULL, 10 );
if( 100 > ret )
{
ret = -1;
}
return ret;
}
char *
tr_httpParse( const char * data, int len, tr_http_header_t *headers )
{
const char * body, * begin;
int ii, jj, full;
/* find the end of the http headers */
body = slice( data, &len, CR LF CR LF );
if( NULL == body )
{
body = slice( data, &len, LF LF );
if( NULL == body )
{
body = slice( data, &len, CR CR );
if( NULL == body )
{
return NULL;
}
}
}
/* return if no headers were requested */
if( NULL == headers || NULL == headers[0].name )
{
return (char*) body;
}
/* NULL out all the header's data pointers */
for( ii = 0; NULL != headers[ii].name; ii++ )
{
headers[ii].data = NULL;
headers[ii].len = 0;
}
/* skip the http request or response line */
ii = 0;
SKIP( ii, len, !NL( data[ii] ) );
SKIP( ii, len, NL( data[ii] ) );
/* find the requested headers */
while(ii < len )
{
/* skip leading spaces and find the header name */
SKIP( ii, len, SP( data[ii] ) );
begin = data + ii;
SKIP( ii, len, ':' != data[ii] && !NL( data[ii] ) );
if( ':' == data[ii] )
{
full = 1;
/* try to match the found header with one of the requested */
for( jj = 0; NULL != headers[jj].name; jj++ )
{
if( NULL == headers[jj].data )
{
full = 0;
if( 0 == tr_strncasecmp( headers[jj].name, begin,
( data + ii ) - begin ) )
{
ii++;
/* skip leading whitespace and save the header value */
SKIP( ii, len, SP( data[ii] ) );
headers[jj].data = data + ii;
SKIP( ii, len, !NL( data[ii] ) );
headers[jj].len = ( data + ii ) - headers[jj].data;
break;
}
}
}
if( full )
{
break;
}
/* skip to the end of the header */
SKIP( ii, len, !NL( data[ii] ) );
}
/* skip past the newline */
SKIP( ii, len, NL( data[ii] ) );
}
return (char*)body;
}
static const char *
slice( const char * data, int * len, const char * delim )
{
const char *body;
int dlen;
dlen = strlen( delim );
body = tr_memmem( data, *len, delim, dlen );
if( NULL != body )
{
*len = body - data;
body += dlen;
}
return body;
}
int
tr_httpIsUrl( const char * url, int len )
{
if( 0 > len )
{
len = strlen( url );
}
/* check for protocol */
if( 7 > len || 0 != tr_strncasecmp( url, "http://", 7 ) )
{
return 0;
}
return 7;
}
int
tr_httpParseUrl( const char * url, int len,
char ** host, int * port, char ** path )
{
const char * pathstart, * hostend;
int ii, colon, portnum;
char str[6];
if( 0 > len )
{
len = strlen( url );
}
ii = tr_httpIsUrl( url, len );
if( 0 >= ii )
{
return 1;
}
url += ii;
len -= ii;
/* find the hostname and port */
colon = -1;
for( ii = 0; len > ii && '/' != url[ii]; ii++ )
{
if( ':' == url[ii] )
{
colon = ii;
}
}
hostend = url + ( 0 > colon ? ii : colon );
pathstart = url + ii;
/* parse the port number */
portnum = HTTP_PORT;
if( 0 <= colon )
{
colon++;
memset( str, 0, sizeof( str ) );
memcpy( str, url + colon, MIN( (int) sizeof( str) - 1, ii - colon ) );
portnum = strtol( str, NULL, 10 );
if( 0 >= portnum || 0xffff <= portnum )
{
tr_err( "Invalid port (%i)", portnum );
return 1;
}
}
if( NULL != host )
{
*host = tr_strndup( url, hostend - url );
}
if( NULL != port )
{
*port = portnum;
}
if( NULL != path )
{
if( 0 < len - ( pathstart - url ) )
{
*path = tr_strndup( pathstart, len - ( pathstart - url ) );
}
else
{
*path = strdup( "/" );
}
}
return 0;
}
tr_http_t *
tr_httpClient( int method, const char * host, int port, const char * fmt, ... )
{
tr_http_t * http;
va_list ap1, ap2;
char * methodstr;
http = malloc( sizeof( *http ) );
if( NULL == http )
{
return NULL;
}
memset( http, 0, sizeof( *http ) );
http->state = HTTP_STATE_CONSTRUCT;
http->lengthtype = HTTP_LENGTH_UNKNOWN;
http->host = strdup( host );
http->port = port;
http->sock = -1;
if( NULL == http->host || NULL == fmt )
{
goto err;
}
switch( method )
{
case TR_HTTP_GET:
methodstr = "GET";
break;
case TR_HTTP_POST:
methodstr = "POST";
break;
case TR_HTTP_M_POST:
methodstr = "M-POST";
break;
default:
goto err;
}
if( tr_sprintf( EXPANDBUF( http->header ), "%s ", methodstr ) )
{
goto err;
}
va_start( ap1, fmt );
va_start( ap2, fmt );
if( tr_vsprintf( EXPANDBUF( http->header ), fmt, ap1, ap2 ) )
{
va_end( ap2 );
va_end( ap1 );
goto err;
}
va_end( ap2 );
va_end( ap1 );
if( tr_sprintf( EXPANDBUF( http->header ), " HTTP/1.1" CR LF
"Host: %s:%d" CR LF
"User-Agent: " TR_NAME "/" LONG_VERSION_STRING CR LF
"Connection: close" CR LF,
http->host, http->port ) )
{
goto err;
}
return http;
err:
tr_httpClose( http );
return NULL;
}
tr_http_t *
tr_httpClientUrl( int method, const char * fmt, ... )
{
char * url, * host, * path;
int port;
va_list ap;
tr_http_t * ret;
va_start( ap, fmt );
url = NULL;
vasprintf( &url, fmt, ap );
va_end( ap );
if( tr_httpParseUrl( url, -1, &host, &port, &path ) )
{
tr_err( "Invalid HTTP URL: %s", url );
free( url );
return NULL;
}
free( url );
ret = tr_httpClient( method, host, port, "%s", path );
free( host );
free( path );
return ret;
}
void
tr_httpAddHeader( tr_http_t * http, const char * name, const char * value )
{
if( HTTP_STATE_CONSTRUCT == http->state )
{
if( tr_sprintf( EXPANDBUF( http->header ),
"%s: %s" CR LF, name, value ) )
{
http->state = HTTP_STATE_ERROR;
}
}
else
{
assert( HTTP_STATE_ERROR == http->state );
}
}
void
tr_httpAddBody( tr_http_t * http , const char * fmt , ... )
{
va_list ap1, ap2;
if( HTTP_STATE_CONSTRUCT == http->state )
{
va_start( ap1, fmt );
va_start( ap2, fmt );
if( tr_vsprintf( EXPANDBUF( http->body ), fmt, ap1, ap2 ) )
{
http->state = HTTP_STATE_ERROR;
}
va_end( ap2 );
va_end( ap1 );
}
else
{
assert( HTTP_STATE_ERROR == http->state );
}
}
void
tr_httpGetHeaders( tr_http_t * http, const char ** buf, int * len )
{
*buf = http->header.buf;
*len = http->header.used;
}
void
tr_httpGetBody( tr_http_t * http, const char ** buf, int * len )
{
*buf = http->body.buf;
*len = http->body.used;
}
tr_tristate_t
tr_httpPulse( tr_http_t * http, const char ** data, int * len )
{
struct in_addr addr;
switch( http->state )
{
case HTTP_STATE_CONSTRUCT:
if( tr_sprintf( EXPANDBUF( http->header ), "Content-length: %i"
CR LF CR LF, http->body.used ) )
{
goto err;
}
if( !tr_netResolve( http->host, &addr ) )
{
http->sock = tr_netOpenTCP( &addr, htons( http->port ), 1 );
http->state = HTTP_STATE_CONNECT;
break;
}
http->resolve = tr_netResolveInit( http->host );
if( NULL == http->resolve )
{
goto err;
}
http->state = HTTP_STATE_RESOLVE;
/* fallthrough */
case HTTP_STATE_RESOLVE:
switch( tr_netResolvePulse( http->resolve, &addr ) )
{
case TR_NET_WAIT:
return TR_NET_WAIT;
case TR_NET_ERROR:
goto err;
case TR_NET_OK:
tr_netResolveClose( http->resolve );
http->resolve = NULL;
http->sock = tr_netOpenTCP( &addr, htons( http->port ), 1 );
http->state = HTTP_STATE_CONNECT;
}
/* fallthrough */
case HTTP_STATE_CONNECT:
switch( sendrequest( http ) )
{
case TR_NET_WAIT:
return TR_NET_WAIT;
case TR_NET_ERROR:
goto err;
case TR_NET_OK:
http->state = HTTP_STATE_RECEIVE;
}
/* fallthrough */
case HTTP_STATE_RECEIVE:
switch( receiveresponse( http ) )
{
case TR_NET_WAIT:
return TR_NET_WAIT;
case TR_NET_ERROR:
goto err;
case TR_NET_OK:
goto ok;
}
break;
case HTTP_STATE_DONE:
goto ok;
case HTTP_STATE_ERROR:
goto err;
}
return TR_NET_WAIT;
err:
http->state = HTTP_STATE_ERROR;
return TR_NET_ERROR;
ok:
http->state = HTTP_STATE_DONE;
if( NULL != data )
{
*data = http->header.buf;
}
if( NULL != len )
{
*len = http->header.used;
}
return TR_NET_OK;
}
static tr_tristate_t
sendrequest( tr_http_t * http )
{
struct buf * buf;
if( !http->date )
http->date = tr_date();
if( 0 > http->sock || tr_date() > http->date + HTTP_TIMEOUT )
{
return TR_NET_ERROR;
}
buf = ( 0 < http->header.used ? &http->header : &http->body );
while( 0 < buf->used )
{
const int ret = tr_netSend( http->sock, buf->buf, buf->used );
if( ret & TR_NET_CLOSE ) return TR_NET_ERROR;
if( ret & TR_NET_BLOCK ) return TR_NET_WAIT;
buf->used = 0;
buf = &http->body;
}
free( http->body.buf );
http->body.buf = NULL;
http->body.size = 0;
http->date = 0;
return TR_NET_OK;
}
static tr_tristate_t
receiveresponse( tr_http_t * http )
{
int ret, before;
void * newbuf;
if( 0 == http->date )
{
http->date = tr_date();
}
before = http->header.used;
for(;;)
{
if( http->header.size - http->header.used < HTTP_BUFSIZE )
{
newbuf = realloc( http->header.buf,
http->header.size + HTTP_BUFSIZE );
if( NULL == newbuf )
{
return TR_NET_ERROR;
}
http->header.buf = newbuf;
http->header.size += HTTP_BUFSIZE;
}
ret = tr_netRecv( http->sock,
(uint8_t *) ( http->header.buf + http->header.used ),
http->header.size - http->header.used );
if( ret & TR_NET_CLOSE )
{
checklength( http );
return TR_NET_OK;
}
else if( ret & TR_NET_BLOCK )
{
break;
}
else
{
http->header.used += ret;
}
}
if( before < http->header.used && checklength( http ) )
{
return TR_NET_OK;
}
if( tr_date() > HTTP_TIMEOUT + http->date )
{
return TR_NET_ERROR;
}
return TR_NET_WAIT;
}
static int
checklength( tr_http_t * http )
{
char * buf;
int num, ii, len, lastnum;
switch( http->lengthtype )
{
case HTTP_LENGTH_UNKNOWN:
if( learnlength( http ) )
{
return checklength( http );
}
break;
case HTTP_LENGTH_EOF:
break;
case HTTP_LENGTH_FIXED:
if( http->header.used >= http->chunkoff + http->chunklen )
{
http->header.used = http->chunkoff + http->chunklen;
return 1;
}
break;
case HTTP_LENGTH_CHUNKED:
buf = http->header.buf;
lastnum = -1;
while( http->header.used > http->chunkoff + http->chunklen )
{
num = http->chunkoff + http->chunklen;
if( lastnum == num )
{
/* ugh, some trackers send Transfer-encoding: chunked
and then don't encode the body */
http->lengthtype = HTTP_LENGTH_EOF;
return checklength( http );
}
lastnum = num;
while( http->header.used > num && NL( buf[num] ) )
{
num++;
}
ii = num;
while( http->header.used > ii && !NL( buf[ii] ) )
{
ii++;
}
if( http->header.used > ii )
{
/* strtol should stop at the newline */
len = strtol( buf + num, NULL, 16 );
if( 0 == len )
{
/* XXX should handle invalid length
differently than 0 length chunk */
http->header.used = http->chunkoff + http->chunklen;
return 1;
}
if( http->header.used > ii + 1 )
{
ii += ( 0 == memcmp( buf + ii, CR LF, 2 ) ? 2 : 1 );
if( http->header.used > ii )
{
memmove( buf + http->chunkoff + http->chunklen,
buf + ii, http->header.used - ii );
}
http->header.used -=
ii - ( http->chunkoff + http->chunklen );
http->chunkoff += http->chunklen;
http->chunklen = len;
}
}
}
break;
}
return 0;
}
static int
learnlength( tr_http_t * http )
{
tr_http_header_t hdr[] = {
{ "Content-Length", NULL, 0 },
/*
XXX this probably doesn't handle multiple encodings correctly
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.41
*/
{ "Transfer-Encoding", NULL, 0 },
{ NULL, NULL, 0 }
};
const char * body;
char * duped;
body = tr_httpParse( http->header.buf, http->header.used, hdr );
if( NULL != body )
{
if( 0 < hdr[1].len &&
0 == tr_strncasecmp( "chunked", hdr[1].data, hdr[1].len ) )
{
http->lengthtype = HTTP_LENGTH_CHUNKED;
http->chunkoff = body - http->header.buf;
http->chunklen = 0;
}
else if( 0 < hdr[0].len )
{
http->lengthtype = HTTP_LENGTH_FIXED;
http->chunkoff = body - http->header.buf;
duped = tr_strndup( hdr[0].data, hdr[0].len );
http->chunklen = strtol( duped, NULL, 10 );
free( duped );
}
else
{
http->lengthtype = HTTP_LENGTH_EOF;
}
return 1;
}
return 0;
}
char *
tr_httpWhatsMyAddress( tr_http_t * http )
{
struct sockaddr_in sin;
socklen_t size;
char buf[INET_ADDRSTRLEN];
if( 0 > http->sock )
{
return NULL;
}
size = sizeof( sin );
if( 0 > getsockname( http->sock, (struct sockaddr *) &sin, &size ) )
{
return NULL;
}
tr_netNtop( &sin.sin_addr, buf, sizeof( buf ) );
return strdup( buf );
}
void
tr_httpClose( tr_http_t * http )
{
if( NULL != http->resolve )
{
tr_netResolveClose( http->resolve );
}
free( http->host );
if( 0 <= http->sock )
{
tr_netClose( http->sock );
}
free( http->header.buf );
free( http->body.buf );
free( http );
}