diff --git a/ot_http.c b/ot_http.c new file mode 100644 index 0000000..0ae14c1 --- /dev/null +++ b/ot_http.c @@ -0,0 +1,544 @@ +/* This software was written by Dirk Engling + It is considered beerware. Prost. Skol. Cheers or whatever. */ + +/* System */ +#include +#include +#include +#include +#include +#include + +/* Libowfat */ +#include "byte.h" +#include "array.h" +#include "iob.h" + +/* Opentracker */ +#include "trackerlogic.h" +#include "ot_mutex.h" +#include "ot_http.h" +#include "ot_iovec.h" +#include "scan_urlencoded_query.h" +#include "ot_fullscrape.h" +#include "ot_stats.h" +#include "ot_accesslist.h" +#include "ot_sync.h" + +#ifndef WANT_TRACKER_SYNC +#define add_peer_to_torrent(A,B,C) add_peer_to_torrent(A,B) +#endif + +#define OT_MAXMULTISCRAPE_COUNT 64 +static ot_hash multiscrape_buf[OT_MAXMULTISCRAPE_COUNT]; + +enum { + SUCCESS_HTTP_HEADER_LENGTH = 80, + SUCCESS_HTTP_HEADER_LENGHT_CONTENT_ENCODING = 32, + SUCCESS_HTTP_SIZE_OFF = 17 }; + +/* Our static output buffer */ +static char static_outbuf[8192]; +#ifdef _DEBUG_HTTPERROR +static char debug_request[8192]; +#endif + +static void http_senddata( const int64 client_socket, char *buffer, size_t size ) { + struct http_data *h = io_getcookie( client_socket ); + ssize_t written_size; + + /* whoever sends data is not interested in its input-array */ + if( h && ( h->flag & STRUCT_HTTP_FLAG_ARRAY_USED ) ) { + h->flag &= ~STRUCT_HTTP_FLAG_ARRAY_USED; + array_reset( &h->request ); + } + + written_size = write( client_socket, buffer, size ); + if( ( written_size < 0 ) || ( (size_t)written_size == size ) ) { + free( h ); io_close( client_socket ); + } else { + char * outbuf; + tai6464 t; + + if( !h ) return; + if( !( outbuf = malloc( size - written_size ) ) ) { + free(h); io_close( client_socket ); + return; + } + + iob_reset( &h->batch ); + memmove( outbuf, buffer + written_size, size - written_size ); + iob_addbuf_free( &h->batch, outbuf, size - written_size ); + h->flag |= STRUCT_HTTP_FLAG_IOB_USED; + + /* writeable short data sockets just have a tcp timeout */ + taia_uint( &t, 0 ); io_timeout( client_socket, t ); + io_dontwantread( client_socket ); + io_wantwrite( client_socket ); + } +} + +#define HTTPERROR_400 return http_issue_error( client_socket, "400 Invalid Request", "This server only understands GET." ) +#define HTTPERROR_400_PARAM return http_issue_error( client_socket, "400 Invalid Request", "Invalid parameter" ) +#define HTTPERROR_400_COMPACT return http_issue_error( client_socket, "400 Invalid Request", "This server only delivers compact results." ) +#define HTTPERROR_403_IP return http_issue_error( client_socket, "403 Access Denied", "Your ip address is not allowed to administrate this server." ) +#define HTTPERROR_404 return http_issue_error( client_socket, "404 Not Found", "No such file or directory." ) +#define HTTPERROR_500 return http_issue_error( client_socket, "500 Internal Server Error", "A server error has occured. Please retry later." ) +ssize_t http_issue_error( const int64 client_socket, const char *title, const char *message ) { + size_t reply_size = sprintf( static_outbuf, + "HTTP/1.0 %s\r\nContent-Type: text/html\r\nConnection: close\r\nContent-Length: %zd\r\n\r\n%s\n", + title, strlen(message)+strlen(title)+16-4,title+4); +#ifdef _DEBUG_HTTPERROR + fprintf( stderr, "DEBUG: invalid request was: %s\n", debug_request ); +#endif + http_senddata( client_socket, static_outbuf, reply_size); + return -2; +} + +ssize_t http_sendiovecdata( const int64 client_socket, int iovec_entries, struct iovec *iovector ) { + struct http_data *h = io_getcookie( client_socket ); + char *header; + int i; + size_t header_size, size = iovec_length( &iovec_entries, &iovector ); + tai6464 t; + + /* No cookie? Bad socket. Leave. */ + if( !h ) { + iovec_free( &iovec_entries, &iovector ); + HTTPERROR_500; + } + + /* If this socket collected request in a buffer, + free it now */ + if( h->flag & STRUCT_HTTP_FLAG_ARRAY_USED ) { + h->flag &= ~STRUCT_HTTP_FLAG_ARRAY_USED; + array_reset( &h->request ); + } + + /* If we came here, wait for the answer is over */ + h->flag &= ~STRUCT_HTTP_FLAG_WAITINGFORTASK; + + /* Our answers never are 0 vectors. Return an error. */ + if( !iovec_entries ) { + HTTPERROR_500; + } + + /* Prepare space for http header */ + header = malloc( SUCCESS_HTTP_HEADER_LENGTH + SUCCESS_HTTP_HEADER_LENGHT_CONTENT_ENCODING ); + if( !header ) { + iovec_free( &iovec_entries, &iovector ); + HTTPERROR_500; + } + + if( h->flag & STRUCT_HTTP_FLAG_GZIP ) + header_size = sprintf( header, "HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\nContent-Encoding: gzip\r\nContent-Length: %zd\r\n\r\n", size ); + else if( h->flag & STRUCT_HTTP_FLAG_BZIP2 ) + header_size = sprintf( header, "HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\nContent-Encoding: bzip2\r\nContent-Length: %zd\r\n\r\n", size ); + else + header_size = sprintf( header, "HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\nContent-Length: %zd\r\n\r\n", size ); + + iob_reset( &h->batch ); + iob_addbuf_free( &h->batch, header, header_size ); + + /* Will move to ot_iovec.c */ + for( i=0; ibatch, iovector[i].iov_base, iovector[i].iov_len ); + free( iovector ); + + h->flag |= STRUCT_HTTP_FLAG_IOB_USED; + + /* writeable sockets timeout after twice the pool timeout + which defaults to 5 minutes (e.g. after 10 minutes) */ + taia_now( &t ); taia_addsec( &t, &t, OT_CLIENT_TIMEOUT_SEND ); + io_timeout( client_socket, t ); + io_dontwantread( client_socket ); + io_wantwrite( client_socket ); + return 0; +} + +#ifdef WANT_TRACKER_SYNC +static ssize_t http_handle_sync( const int64 client_socket, char *data ) { + struct http_data* h = io_getcookie( client_socket ); + size_t len; + int mode = SYNC_OUT, scanon = 1; + char *c = data; + + if( !accesslist_isblessed( h->ip, OT_PERMISSION_MAY_SYNC ) ) + HTTPERROR_403_IP; + + while( scanon ) { + switch( scan_urlencoded_query( &c, data = c, SCAN_SEARCHPATH_PARAM ) ) { + case -2: scanon = 0; break; /* TERMINATOR */ + case -1: HTTPERROR_400_PARAM; /* PARSE ERROR */ + default: scan_urlencoded_skipvalue( &c ); break; + case 9: + if(byte_diff(data,9,"changeset")) { + scan_urlencoded_skipvalue( &c ); + continue; + } + /* ignore this, when we dont at least see "d4:syncdee" */ + if( ( len = scan_urlencoded_query( &c, data = c, SCAN_SEARCHPATH_VALUE ) ) < 10 ) HTTPERROR_400_PARAM; + if( add_changeset_to_tracker( (uint8_t*)data, len ) ) HTTPERROR_400_PARAM; + if( mode == SYNC_OUT ) { + stats_issue_event( EVENT_SYNC_IN, 1, 0 ); + mode = SYNC_IN; + } + break; + } + } + + if( mode == SYNC_OUT ) { + /* Pass this task to the worker thread */ + h->flag |= STRUCT_HTTP_FLAG_WAITINGFORTASK; + stats_issue_event( EVENT_SYNC_OUT_REQUEST, 1, 0 ); + sync_deliver( client_socket ); + io_dontwantread( client_socket ); + return -2; + } + + /* Simple but proof for now */ + memmove( static_outbuf + SUCCESS_HTTP_HEADER_LENGTH, "OK", 2); + return 2; +} +#endif + +static ssize_t http_handle_stats( const int64 client_socket, char *data, char *d, size_t l ) { + struct http_data* h = io_getcookie( client_socket ); + char *c = data; + int mode = TASK_STATS_PEERS, scanon = 1, format = 0; + + while( scanon ) { + switch( scan_urlencoded_query( &c, data = c, SCAN_SEARCHPATH_PARAM ) ) { + case -2: scanon = 0; break; /* TERMINATOR */ + case -1: HTTPERROR_400_PARAM; /* PARSE ERROR */ + default: scan_urlencoded_skipvalue( &c ); break; + case 4: + if( byte_diff(data,4,"mode")) { + scan_urlencoded_skipvalue( &c ); + continue; + } + if( scan_urlencoded_query( &c, data = c, SCAN_SEARCHPATH_VALUE ) != 4 ) HTTPERROR_400_PARAM; + if( !byte_diff(data,4,"peer")) + mode = TASK_STATS_PEERS; + else if( !byte_diff(data,4,"conn")) + mode = TASK_STATS_CONNS; + else if( !byte_diff(data,4,"top5")) + mode = TASK_STATS_TOP5; + else if( !byte_diff(data,4,"scrp")) + mode = TASK_STATS_SCRAPE; + else if( !byte_diff(data,4,"fscr")) + mode = TASK_STATS_FULLSCRAPE; + else if( !byte_diff(data,4,"tcp4")) + mode = TASK_STATS_TCP; + else if( !byte_diff(data,4,"udp4")) + mode = TASK_STATS_UDP; + else if( !byte_diff(data,4,"s24s")) + mode = TASK_STATS_SLASH24S; + else if( !byte_diff(data,4,"tpbs")) + mode = TASK_STATS_TPB; + else + HTTPERROR_400_PARAM; + break; + case 6: + if( byte_diff(data,6,"format")) { + scan_urlencoded_skipvalue( &c ); + continue; + } + if( scan_urlencoded_query( &c, data = c, SCAN_SEARCHPATH_VALUE ) != 3 ) HTTPERROR_400_PARAM; + if( !byte_diff(data,3,"bin")) + format = TASK_FULLSCRAPE_TPB_BINARY; + else if( !byte_diff(data,3,"ben")) + format = TASK_FULLSCRAPE; + else if( !byte_diff(data,3,"url")) + format = TASK_FULLSCRAPE_TPB_URLENCODED; + else if( !byte_diff(data,3,"txt")) + format = TASK_FULLSCRAPE_TPB_ASCII; + else + HTTPERROR_400_PARAM; + break; + } + } + + if( mode == TASK_STATS_TPB ) { + tai6464 t; +#ifdef WANT_COMPRESSION_GZIP + d[l-1] = 0; + if( strstr( d, "gzip" ) ) { + h->flag |= STRUCT_HTTP_FLAG_GZIP; + format |= TASK_FLAG_GZIP; + } +#endif + /* Pass this task to the worker thread */ + h->flag |= STRUCT_HTTP_FLAG_WAITINGFORTASK; + + /* Clients waiting for us should not easily timeout */ + taia_uint( &t, 0 ); io_timeout( client_socket, t ); + fullscrape_deliver( client_socket, format ); + io_dontwantread( client_socket ); + return -2; + } + + // default format for now + if( !( l = return_stats_for_tracker( static_outbuf + SUCCESS_HTTP_HEADER_LENGTH, mode, 0 ) ) ) HTTPERROR_500; + return l; +} + +static ssize_t http_handle_fullscrape( const int64 client_socket, char *d, size_t l ) { + struct http_data* h = io_getcookie( client_socket ); + int format = 0; + tai6464 t; + +#ifdef WANT_COMPRESSION_GZIP + d[l-1] = 0; + if( strstr( d, "gzip" ) ) { + h->flag |= STRUCT_HTTP_FLAG_GZIP; + format = TASK_FLAG_GZIP; + stats_issue_event( EVENT_FULLSCRAPE_REQUEST_GZIP, *(int*)h->ip, 0 ); + } else +#endif + stats_issue_event( EVENT_FULLSCRAPE_REQUEST, *(int*)h->ip, 0 ); + +#ifdef _DEBUG_HTTPERROR +write( 2, debug_request, l ); +#endif + + /* Pass this task to the worker thread */ + h->flag |= STRUCT_HTTP_FLAG_WAITINGFORTASK; + /* Clients waiting for us should not easily timeout */ + taia_uint( &t, 0 ); io_timeout( client_socket, t ); + fullscrape_deliver( client_socket, TASK_FULLSCRAPE | format ); + io_dontwantread( client_socket ); + return -2; +} + +static ssize_t http_handle_scrape( const int64 client_socket, char *data ) { + int scanon = 1, numwant = 0; + char *c = data; + size_t l; + + /* This is to hack around stupid clients that send "scrape ?info_hash" */ + if( c[-1] != '?' ) { + while( ( *c != '?' ) && ( *c != '\n' ) ) ++c; + if( *c == '\n' ) HTTPERROR_400_PARAM; + ++c; + } + + while( scanon ) { + switch( scan_urlencoded_query( &c, data = c, SCAN_SEARCHPATH_PARAM ) ) { + case -2: scanon = 0; break; /* TERMINATOR */ + case -1: + if( numwant ) + goto UTORRENT1600_WORKAROUND; + HTTPERROR_400_PARAM; /* PARSE ERROR */ + default: scan_urlencoded_skipvalue( &c ); break; + case 9: + if(byte_diff(data,9,"info_hash")) { + scan_urlencoded_skipvalue( &c ); + continue; + } + /* ignore this, when we have less than 20 bytes */ + if( scan_urlencoded_query( &c, data = c, SCAN_SEARCHPATH_VALUE ) != (ssize_t)sizeof(ot_hash) ) { +#ifdef WANT_UTORRENT1600_WORKAROUND + if( data[20] != '?' ) +#endif + HTTPERROR_400_PARAM; + } + if( numwant < OT_MAXMULTISCRAPE_COUNT ) + memmove( multiscrape_buf + numwant++, data, sizeof(ot_hash) ); + break; + } + } + +UTORRENT1600_WORKAROUND: + + /* No info_hash found? Inform user */ + if( !numwant ) HTTPERROR_400_PARAM; + + /* Enough for http header + whole scrape string */ + if( !( l = return_tcp_scrape_for_torrent( multiscrape_buf, numwant, SUCCESS_HTTP_HEADER_LENGTH + static_outbuf ) ) ) HTTPERROR_500; + stats_issue_event( EVENT_SCRAPE, 1, l ); + return l; +} + +static ssize_t http_handle_announce( const int64 client_socket, char *data ) { + char *c = data; + int numwant, tmp, scanon; + ot_peer peer; + ot_torrent *torrent; + ot_hash *hash = NULL; + unsigned short port = htons(6881); + ssize_t len; + + /* This is to hack around stupid clients that send "announce ?info_hash" */ + if( c[-1] != '?' ) { + while( ( *c != '?' ) && ( *c != '\n' ) ) ++c; + if( *c == '\n' ) HTTPERROR_400_PARAM; + ++c; + } + + OT_SETIP( &peer, ((struct http_data*)io_getcookie( client_socket ) )->ip ); + OT_SETPORT( &peer, &port ); + OT_FLAG( &peer ) = 0; + numwant = 50; + scanon = 1; + + while( scanon ) { + switch( scan_urlencoded_query( &c, data = c, SCAN_SEARCHPATH_PARAM ) ) { + case -2: scanon = 0; break; /* TERMINATOR */ + case -1: HTTPERROR_400_PARAM; /* PARSE ERROR */ + default: scan_urlencoded_skipvalue( &c ); break; +#ifdef WANT_IP_FROM_QUERY_STRING + case 2: + if(!byte_diff(data,2,"ip")) { + unsigned char ip[4]; + len = scan_urlencoded_query( &c, data = c, SCAN_SEARCHPATH_VALUE ); + if( ( len <= 0 ) || scan_fixed_ip( data, len, ip ) ) HTTPERROR_400_PARAM; + OT_SETIP( &peer, ip ); + } else + scan_urlencoded_skipvalue( &c ); + break; +#endif + case 4: + if( !byte_diff( data, 4, "port" ) ) { + len = scan_urlencoded_query( &c, data = c, SCAN_SEARCHPATH_VALUE ); + if( ( len <= 0 ) || scan_fixed_int( data, len, &tmp ) || ( tmp > 0xffff ) ) HTTPERROR_400_PARAM; + port = htons( tmp ); OT_SETPORT( &peer, &port ); + } else if( !byte_diff( data, 4, "left" ) ) { + if( ( len = scan_urlencoded_query( &c, data = c, SCAN_SEARCHPATH_VALUE ) ) <= 0 ) HTTPERROR_400_PARAM; + if( scan_fixed_int( data, len, &tmp ) ) tmp = 0; + if( !tmp ) OT_FLAG( &peer ) |= PEER_FLAG_SEEDING; + } else + scan_urlencoded_skipvalue( &c ); + break; + case 5: + if( byte_diff( data, 5, "event" ) ) + scan_urlencoded_skipvalue( &c ); + else switch( scan_urlencoded_query( &c, data = c, SCAN_SEARCHPATH_VALUE ) ) { + case -1: + HTTPERROR_400_PARAM; + case 7: + if( !byte_diff( data, 7, "stopped" ) ) OT_FLAG( &peer ) |= PEER_FLAG_STOPPED; + break; + case 9: + if( !byte_diff( data, 9, "completed" ) ) OT_FLAG( &peer ) |= PEER_FLAG_COMPLETED; + default: /* Fall through intended */ + break; + } + break; + case 7: + if(!byte_diff(data,7,"numwant")) { + len = scan_urlencoded_query( &c, data = c, SCAN_SEARCHPATH_VALUE ); + if( ( len <= 0 ) || scan_fixed_int( data, len, &numwant ) ) HTTPERROR_400_PARAM; + if( numwant < 0 ) numwant = 50; + if( numwant > 200 ) numwant = 200; + } else if(!byte_diff(data,7,"compact")) { + len = scan_urlencoded_query( &c, data = c, SCAN_SEARCHPATH_VALUE ); + if( ( len <= 0 ) || scan_fixed_int( data, len, &tmp ) ) HTTPERROR_400_PARAM; + if( !tmp ) HTTPERROR_400_COMPACT; + } else + scan_urlencoded_skipvalue( &c ); + break; + case 9: + if(byte_diff(data,9,"info_hash")) { + scan_urlencoded_skipvalue( &c ); + continue; + } + /* ignore this, when we have less than 20 bytes */ + if( scan_urlencoded_query( &c, data = c, SCAN_SEARCHPATH_VALUE ) != 20 ) HTTPERROR_400_PARAM; + hash = (ot_hash*)data; + break; + } + } + + /* Scanned whole query string */ + if( !hash ) + return sprintf( static_outbuf + SUCCESS_HTTP_HEADER_LENGTH, "d14:failure reason81:Your client forgot to send your torrent's info_hash. Please upgrade your client.e" ); + + if( OT_FLAG( &peer ) & PEER_FLAG_STOPPED ) + len = remove_peer_from_torrent( hash, &peer, SUCCESS_HTTP_HEADER_LENGTH + static_outbuf, 1 ); + else { + torrent = add_peer_to_torrent( hash, &peer, 0 ); + if( !torrent || !( len = return_peers_for_torrent( hash, numwant, SUCCESS_HTTP_HEADER_LENGTH + static_outbuf, 1 ) ) ) HTTPERROR_500; + } + stats_issue_event( EVENT_ANNOUNCE, 1, len); + return len; +} + +ssize_t http_handle_request( const int64 client_socket, char *data, size_t recv_length ) { + char *c, *recv_header=data; + ssize_t reply_size = 0, reply_off, len; + +#ifdef _DEBUG_HTTPERROR + if( recv_length >= sizeof( debug_request ) ) + recv_length = sizeof( debug_request) - 1; + memcpy( debug_request, recv_header, recv_length ); + debug_request[ recv_length ] = 0; +#endif + + /* This one implicitely tests strlen < 5, too -- remember, it is \n terminated */ + if( byte_diff( data, 5, "GET /") ) HTTPERROR_400; + + /* Query string MUST terminate with SP -- we know that theres at least a '\n' where this search terminates */ + for( c = data + 5; *c!=' ' && *c != '\t' && *c != '\n' && *c != '\r'; ++c ) ; + if( *c != ' ' ) HTTPERROR_400; + + /* Skip leading '/' */ + for( c = data+4; *c == '/'; ++c); + + /* Try to parse the request. + In reality we abandoned requiring the url to be correct. This now + only decodes url encoded characters, we check for announces and + scrapes by looking for "a*" or "sc" */ + len = scan_urlencoded_query( &c, data = c, SCAN_PATH ); + + /* If parsing returned an error, leave with not found*/ + if( len <= 0 ) HTTPERROR_404; + + /* This is the hardcore match for announce*/ + if( ( *data == 'a' ) || ( *data == '?' ) ) + reply_size = http_handle_announce( client_socket, c ); + else if( !byte_diff( data, 12, "scrape HTTP/" ) ) + reply_size = http_handle_fullscrape( client_socket, recv_header, recv_length ); + /* This is the hardcore match for scrape */ + else if( !byte_diff( data, 2, "sc" ) ) + reply_size = http_handle_scrape( client_socket, c ); + /* All the rest is matched the standard way */ + else switch( len ) { +#ifdef WANT_TRACKER_SYNC + case 4: /* sync ? */ + if( byte_diff( data, 4, "sync") ) HTTPERROR_404; + reply_size = http_handle_sync( client_socket, c ); + break; +#endif + case 5: /* stats ? */ + if( byte_diff(data,5,"stats")) HTTPERROR_404; + reply_size = http_handle_stats( client_socket, c, recv_header, recv_length ); + break; + default: + HTTPERROR_404; + } + + /* If routines handled sending themselves, just return */ + if( reply_size == -2 ) return 0; + /* If routine failed, let http error take over */ + if( reply_size == -1 ) HTTPERROR_500; + + /* This one is rather ugly, so I take you step by step through it. + + 1. In order to avoid having two buffers, one for header and one for content, we allow all above functions from trackerlogic to + write to a fixed location, leaving SUCCESS_HTTP_HEADER_LENGTH bytes in our static buffer, which is enough for the static string + plus dynamic space needed to expand our Content-Length value. We reserve SUCCESS_HTTP_SIZE_OFF for its expansion and calculate + the space NOT needed to expand in reply_off + */ + reply_off = SUCCESS_HTTP_SIZE_OFF - snprintf( static_outbuf, 0, "%zd", reply_size ); + + /* 2. Now we sprintf our header so that sprintf writes its terminating '\0' exactly one byte before content starts. Complete + packet size is increased by size of header plus one byte '\n', we will copy over '\0' in next step */ + reply_size += 1 + sprintf( static_outbuf + reply_off, "HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\nContent-Length: %zd\r\n\r", reply_size ); + + /* 3. Finally we join both blocks neatly */ + static_outbuf[ SUCCESS_HTTP_HEADER_LENGTH - 1 ] = '\n'; + + http_senddata( client_socket, static_outbuf + reply_off, reply_size ); + return reply_size; +} diff --git a/ot_http.h b/ot_http.h new file mode 100644 index 0000000..84b9028 --- /dev/null +++ b/ot_http.h @@ -0,0 +1,28 @@ +/* This software was written by Dirk Engling + It is considered beerware. Prost. Skol. Cheers or whatever. */ + +#ifndef __OT_HTTP_H__ +#define __OT_HTTP_H__ + +typedef enum { + STRUCT_HTTP_FLAG_ARRAY_USED = 1, + STRUCT_HTTP_FLAG_IOB_USED = 2, + STRUCT_HTTP_FLAG_WAITINGFORTASK = 4, + STRUCT_HTTP_FLAG_GZIP = 8, + STRUCT_HTTP_FLAG_BZIP2 = 16 +} STRUCT_HTTP_FLAG; + +struct http_data { + union { + array request; + io_batch batch; + }; + char ip[4]; + STRUCT_HTTP_FLAG flag; +}; + +ssize_t http_handle_request( const int64 s, char *data, size_t l ); +ssize_t http_sendiovecdata( const int64 s, int iovec_entries, struct iovec *iovector ); +ssize_t http_issue_error( const int64 s, const char *title, const char *message ); + +#endif \ No newline at end of file