--- /dev/null
+/* This software was written by Dirk Engling <erdgeist@erdgeist.org>
+ It is considered beerware. Prost. Skol. Cheers or whatever. */
+
+/* System */
+#include <sys/types.h>
+#include <sys/uio.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+/* 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<title>%s</title>\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; i<iovec_entries; ++i )
+ iob_addbuf_munmap( &h->batch, 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;
+}