/* ========================================================================== ** * stibtest.c * * Copyright: * Copyright (C) 2007,2011 by Christopher R. Hertel * * Email: crh@ubiqx.mn.org * * $Id: stibtest.c 59 2011-06-28 02:11:49Z crh $ * * -------------------------------------------------------------------------- ** * * Description: * A utility for testing STiBlib and the BITS protocol itself. * * -------------------------------------------------------------------------- ** * * License: * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful. * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. * * -------------------------------------------------------------------------- ** * * Notes: * * This is a fairly simple test program used to exercise the STiB client * library, stibclilib. * * Basic usage is: stibtest [command options] * For example, to send a BITS Ping command to a server, you would enter * something like this: stibtest ping http://server/ * * The program also provides help for all commands. For example: * stibtest help * stibtest help send * stibtest help session * * The top-level commands are: * help - Request help. * ping - send a BITS Ping to the designated server. * send - Use BITS to transfer a file to the designated server. * get - Use HTTP to request content from the designated server. * session - Create a BITS session to send a file to the designated * server, but enter interactive mode so that the user can * manually control the transfer. * * The session mode interactive commands are: * fragment - Send a fragment. Optional parameters allow the user * to set the size of transfer fragment and the offset * within the source file at which the fragment should * start. * disconnect - For testing, this allows the user to disconnect the * TCP socket. The STiB library should automagically * reconnect. * cancel - Cancel an in-progress transfer. * close - Finish a completed transfer. * exit/quit - Force close/cancel and exit even if they fail. * * ToDo: * * Add a "stat[us]" subcommand to show the verbosity level, the current * status of the session (number of bytes transferred, etc.) and the * default transfer size. * * * Add a "finish" subcommand to complete a transfer started with the * session command. * * * Clean up and standardize diagnostic levels. * * * Add actual support for secure (HTTPS) transfers. * * * Consider replacing dumpBlock() with hexDump()? * * ========================================================================== ** */ #include /* For isprint(3). */ #include /* Getopt(3) stuff & fstat(2). */ #include /* For fstat(2). */ #include /* For fstat(2). */ #include /* GNU Readline command editing. */ #include /* GNU Readline command history. */ #include "util.h" /* Utility functions and more headers. */ #include "stibclilib.h" /* STiB library of BITS functions. */ /* -------------------------------------------------------------------------- ** * Defined Constants: * * bSIZE - File read buffer size. */ #define bSIZE 4096 /* -------------------------------------------------------------------------- ** * Macros: * * ifNotThen( A, B ) - If A is "true" then return A, else return B. * vLvl( V ) - If the verbosity level is V or higher, then true. * isEmpty( S ) - If the string passed as is NULL or the empty * string (""), return (meaning "yes, it is * empty"). Otherwise return . */ #define ifNotThen( A, B ) ((A)?(A):(B)) #define vLvl( V ) (Verbose >= (V)) #define isEmpty( S ) ( ((NULL==(S))||('\0'==(*(S)))) ? true : false ) /* -------------------------------------------------------------------------- ** * Typedefs: * * cmdID - An enumerated set of numeric IDs to identify commands. * * sessCmdID - An enumerated set of numeric IDs to identify session * commands, which are subcommands of the 'session' * command. * * cmdName2IDMap - Used to map command name strings to the IDs that * represent those commands. This structure is used for * both commands and subcommands. */ typedef enum { cmd_Help = 0, /* A request for help. */ cmd_Ping, /* Send a BITS ping to a server. */ cmd_Send, /* Send a file to a BITS server. */ cmd_Get, /* Get a file from an HTTP server. */ cmd_Sess, /* Create a BITS file transfer session. */ cmd_MAX /* End of the list. */ } cmdID; typedef enum { scmd_Help = 0, /* A request for help. */ scmd_Verbosity, /* Adjust verbosity level. */ scmd_Fragment, /* Send a fragment. */ scmd_Disconnect, /* Disconnect the session. */ scmd_Cancel, /* Cancel a transfer before completion. */ scmd_Close, /* Complete a transfer. */ scmd_Quit, /* Cancel/Close and force exit. */ scmd_MAX /* End of the list. */ } sessCmdID; typedef struct { char *name; /* The command name; a string. */ int minmatch; /* Minimum number of bytes needed to recognize a command. */ int ID; /* cmdID or sessCmdID of the command. */ } cmdName2IDMap; /* -------------------------------------------------------------------------- ** * Static Constants: * * Agent - Agent name, used in BITS header messages. * Fix: Use util.vString() to extract just the Rev number. * Copyright - Copyright string. * License - License string. * Revision - Revision string. Short form. * ID - Revision and identification string. Long form. * * DefaultPort - The default port to which to connect when connecting to * a server. Given as a string because it is passed to * stib_Connect() as a string. * DefaultSPort - The default port to which to connect when using a secure * connection. Given as a string because it is passed to * stib_Connect() as a string. * DefaultPath - The default remote path name to use if no path name is * given in an input URL. * * cmdMap - A set of mappings from command names to command IDs. * * sessCmdMap - A set of mappings from session subcommand names to their * respecitve IDs. * * helpMsg - The short and sweet general help message. * * verboseMsg - The long and tedious help message. */ static const char *Agent = "ubiqx STiBtest v0.2 ($Revision: 59 $)"; static const char *Copyright = "Copyright (c) 2011 by Christopher R. Hertel"; static const char *License = "License: GNU GPLv2 and GNU LGPLv2.1. See the source."; static const char *Revision = "V0.2; $Revision: 59 $"; static const char *ID = "V0.2; $Id: stibtest.c 59 2011-06-28 02:11:49Z crh $"; static const char *DefaultPort = "80"; static const char *DefaultSPort = "443"; static const char *DefaultPath = "/"; static cmdName2IDMap cmdMap[] = { { "help", 1, cmd_Help }, { "ping", 1, cmd_Ping }, { "send", 4, cmd_Send }, { "get", 1, cmd_Get }, { "session", 4, cmd_Sess }, { NULL, 0, cmd_MAX } }; static cmdName2IDMap sessCmdMap[] = { { "help", 1, scmd_Help }, { "verbosity", 1, scmd_Verbosity }, { "fragment", 1, scmd_Fragment }, { "disconnect", 1, scmd_Disconnect }, { "cancel", 3, scmd_Cancel }, { "close", 2, scmd_Close }, { "exit", 2, scmd_Quit }, { "quit", 1, scmd_Quit }, { NULL, 0, scmd_MAX } }; static const char *helpMsg[] = { "Usage: %s {-v}[-h][-V] [ [command-parameters]]", " Options: -h Provide basic help. -vh = Advanced help.", " -q Be quiet.", " -v Be verbose. -vv = Very verbose, etc.", " -V Provide version info, then exit.", "", " Commands:", " h[elp] - Provide help.", " Use \"help \" for help with a specific command.", " p[ing] - Verify that a server is available and supports BITS.", " send - Send a file to a BITS server.", " g[et] - Use HTTP to request content from the designated server.", " sess[ion] - Create a transfer session and enter interactive mode.", NULL }; static const char *verboseMsg[] = { "Usage: %s {-v}[-h][-V] [ [command-parameters]]", "", " Options: -d Specify the job file directory (default: './').", " This is the location in which STiB will place new job", " files and may find existing job files for jobs that", " are ready to be resumed.", " -h Provide help. -vh = Advanced help (this message).", " -q Be quiet. (Set verbosity level to zero (0).)", " -v Be verbose. -vv = Very verbose, etc.", " The default verbosity level is 1.", " Use -q to set the verbosity level to 0.", " -V Provide version info, then exit.", " -vV produces more detailed version information.", "", " %s can be used to test the behavior of stibclilib, the", " STiB client toolkit library.", "", " Commands:", " h[elp] - Provide help.", " Use \"help \" for help with a specific command.", "", " p[ing] - Verify that a server is available and supports BITS.", " Usage: %s ping ", "", " send - Send a file to a BITS server.", " Usage: %s send ", "", " g[et] - Get a file from an HTTPv1.1 server.", " Usage: %s get [peerdist] [range :]", " ", " Specifying \"peerdist\" will request BranchCache[tm]", " PeerDist encoding from the server.", " The \"range :\" option allows a range of a", " file to be selected. Either or may be", " blank (interpreted as 0), but the colon must be present.", " A of zero is interpreted as meaning \"all remaining", " bytes\", so 0:0 requests the whole file.", "", " sess[ion] - Create a transfer session and enter interactive mode.", " Usage: %s session ", " If the session is created successfully, the program will", " enter interactive mode. Interactive commands are:", " v[erbosity] [[+|-]level]", " Set or adjust the diagnostic verbosity level.", " frag[ment] [[offset] size]", " Send a file fragment to the server using the BITS", " Fragment command.", " d[isconnect]", " Disconnect the TCP connection to the server.", " can[cel]", " Send a BITS Cancel-Session command to the server.", " This will discard the transfer, close the connection,", " and exit interactive mode.", " cl[ose]", " Send a BITS Close-Session command to the server. This", " will complete the transfer, close the connection, and", " exit interactive mode. The Close-Session should only", " be sent after the transfer has completed.", " q[uit] | ex[it]", " Send either a Close-Session or Cancel-Session, depending", " on the state of the transfer. Forces an exit from", " interactive mode, no matter what the result.", " Type \"help\" at the session prompt for more information.", NULL }; static const char *msg_cmdHelp[] = { "help | help ", " Type 'help ' for information about a specific command.", " The commands are: 'help', 'ping', 'send', 'get', and 'session'.", " Typing 'help help' gets you this blurb.", NULL }; static const char *msg_cmdPing[] = { "ping ", " Send a BITS Ping request to a server to determine whether it is", " listening and ready to receive a file.", NULL }; static const char *msg_cmdSend[] = { "send ", " Send a local file to a remote BITS server. This command opens a TCP", " connection to the remote server, creates a BITS session, transfers the", " file, and finally closes the connection. There is no attempt to", " manage the transfer rate. A new fragment is sent upon successful", " receipt of an Fragment ACK.", NULL }; static const char *msg_cmdGet[] = { "get [p[eerdist]] [r[ange] []:[]] " "", " Request a file from a remote HTTPv1.1 server. This command opens a TCP", " connection to the remote server and downloads the requested file (if it", " is available). If the 'peerdist' option is specified, then peerdist", " encoding will be be requested from the server. A range may also be", " specified. The is the starting point within the remote file,", " and the is the number of bytes to request. A of zero", " is interpreted as meaning all of the bytes, starting at . The", " will contain the raw data as it was received from the " "server.", NULL }; static const char *msg_cmdSess[] = { "session ", " Establish a session with a remote BITS server. This command opens a", " TCP connection and sends the initial Create-Session request. If that", " is successful, the program enters an interactive mode.", NULL }; static const char **cmdhelp_Msg[cmd_MAX] = { msg_cmdHelp, msg_cmdPing, msg_cmdSend, msg_cmdGet, msg_cmdSess }; static const char *msg_scmdHelp[] = { "The available session commands are:", " v[erbosity] [[+|-]level] -- Change the verbosity level of diagnostics.", " frag[ment] [[offset] size] -- Send a fragment to the server.", " d[isconnect] -- Disconnect the TCP connection.", " can[cel] -- Cancel the transfer session.", " cl[ose] -- Close the completed transfer session.", " ex[it] | q[uit] -- Force close/cancel and exit.", "Type 'help ' for help with a specific session command.", NULL }; static const char *msg_scmdVerbosity[] = { "v[erbosity] [[+|-]level]", " Set or adjust the diagnostic verbosity level.", " Without any parameters, this command reports the current verbosity level.", " If a numeric value is given, the verbosity level will be set to the given", " value. If the value is preceeded by a plus ('+') or minus ('-') sign,", " the current verbosity level is adjusted accordingly, and the result is", " reported.", NULL }; static const char *msg_scmdFragment[] = { "frag[ment] [[offset] size]", " Send a file fragment to the server using the BITS Fragment command.", " - The parameter sets the maximum number of bytes to be transmitted", " in a single fragment. The default value is 4K bytes.", " - The parameter can be specified to change the offset of the", " fragment being sent. This is useful for testing. If is given", " then must also be given.", NULL }; static const char *msg_scmdDisconnect[] = { "d[isconnect]", " Disconnect the TCP connection to the server, but maintain state.", " This is useful for testing BITS restart behavior.", NULL }; static const char *msg_scmdCancel[] = { "can[cel]", " Send a BITS Cancel-Session command to the server to abort the tranfer.", " The server should discard the data transferred so far. If the server", " sends back a success message (2nn), the client will close the connection", " and exit. Otherwise, the session will remain connected.", NULL }; static const char *msg_scmdClose[] = { "cl[ose]", " Send a BITS Close-Session command to the server to complete the transfer.", " If the close request is successful (status 2nn), the client will close", " the connection, and exit. The Close-Session should only be sent after", " the file transfer has completed.", NULL }; static const char *msg_scmdQuit[] = { "ex[it] | q[uit]", " Send either a Close-Session or Cancel-Session, depending on the state of", " the transfer. This command forces an exit from the program, ignoring the", " result sent by the server. This is useful in testing situations when", " things just aren't going right.", NULL }; static const char **scmdhelp_Msg[scmd_MAX] = { msg_scmdHelp, msg_scmdVerbosity, msg_scmdFragment, msg_scmdDisconnect, msg_scmdCancel, msg_scmdClose, msg_scmdQuit }; /* -------------------------------------------------------------------------- ** * Global Variables: * * progName - Pointer to a copy of the program name. Initialized to * NULL. * Verbose - Used to determine the level of diagnostic and informative * messages that are provided. Default value: 1. */ static char *progName = NULL; static uint Verbose = 1; /* -------------------------------------------------------------------------- ** * Static Functions: */ static bool is2xx( int statuscode ) /* ------------------------------------------------------------------------ ** * Return true if the status returned by the server is in the 2xx range. * * Input: statuscode - The status code value. * * Output: True if is in the 200..299 range, else false. * * ------------------------------------------------------------------------ ** */ { if( (statuscode >= 200) && (statuscode < 300) ) return( true ); return( false ); } /* is2xx */ static off_t newOffset( const char *offstr, const off_t max, FILE *inf ) /* ------------------------------------------------------------------------ ** * Convert a string to an offset value and seek to that position. * * Input: offstr - A string representing a decimal or hexidecimal value * to be converted into integer (off_t) form. * max - The maximum value of the offset. If the converted * value exceeds , or if it is negative, the * function will return -1 to indicate an error. * Typically, would be the file size. * inf - The file in which to seek to a new offset. * * Output: On success, the new offset value is returned. * -1 is returned if the converted value would be either negative * or greater than the value of . * * ------------------------------------------------------------------------ ** */ { long long int tmpll; tmpll = strtoll( offstr, NULL, 0 ); if( (tmpll < 0) || (tmpll > max) ) return( -1 ); (void)fseeko( inf, (off_t)tmpll, SEEK_SET ); return( (off_t)tmpll ); } /* newOffset */ #if 0 Commented out until I figure out if/how I want to use it. static void dumpBlock( Gbfr *mb ) /* ------------------------------------------------------------------------ ** * Print the contents of a message block to stderr. * * Input: mb - A pointer to a growable buffer (Gbfr) structure that * contains the data to be dumped. * * Output: * * ------------------------------------------------------------------------ ** */ { int i; int len; const char *bufr; bufr = bfrGbfr( mb ); len = lenGbfr( mb ); for( i = 0; i < len; i++ ) { if( isprint( bufr[i] ) ) (void)fputc( bufr[i], stderr ); else { switch( bufr[i] ) { case '\n': Err( "\n" ); break; case '\t': Err( "\t" ); break; default: Err( "<%.2X>", (unsigned char)bufr[i] ); break; } } } Err( "\n" ); } /* dumpBlock */ #endif static void dump_pMsg( stib_parsedMsg *pMsg ) /* ------------------------------------------------------------------------ ** * Print the already-parsed server response to a BITS request. * * Input: pMsg - A pointer to a parsed message structure. * * Output: * * ------------------------------------------------------------------------ ** */ { stib_hdrNode *hdr; Err( "%s\n", pMsg->start ); if( vLvl(3) ) { hdr = (stib_hdrNode *)ubi_slFirst( pMsg ); while( NULL != hdr ) { Err( "%s: %s\n", hdr->key, ifNotThen( hdr->val, "" ) ); hdr = (stib_hdrNode *)ubi_slNext( hdr ); } Err( "\n" ); } } /* dump_pMsg */ static char **parseCmdLine( char *line, int *count ) /* ------------------------------------------------------------------------ ** * Parse a command line into an argc/argv style count and pointer array. * * Input: line - The command line to parse. * count - A pointer to an integer to receive the count of * pointers in the resulting array. This MUST be * initialized to 0 before calling this function. * * Output: A pointer to an array of string pointers. The array pointers * point to the parse elements of the input line. * * Notes: This function *MODIFIES* the contents of . * The silly thing is recursive. * * ------------------------------------------------------------------------ ** */ { int i; int tmp; char *cmd; char **array = NULL; /* Consume whitespace. */ for( i = 0; isspace(line[i]); i++ ) line[i] = '\0'; /* Do we have an arg? */ if( '\0' != line[i] ) { cmd = &line[i]; tmp = *count; while( '\0' != line[i] && !isspace( line[i] ) ) i++; (*count)++; if( NULL != (array = parseCmdLine( &line[i], count )) ) array[tmp] = cmd; return( array ); } /* If we got here, then there are no arguments left. */ array = (char **)calloc( ((*count) + 1), sizeof( char * ) ); return( array ); } /* parseCmdLine */ static cmdID getCmdID( const char *name ) /* ------------------------------------------------------------------------ ** * Convert a command name string into a cmdID value. * * Input: name - A pointer to the command name to convert. * * Output: The matching cmdID value, or cmd_MAX if there was no match. * * ------------------------------------------------------------------------ ** */ { int i; for( i = 0; (NULL != cmdMap[i].name); i++ ) { if( prefixMatch( name, cmdMap[i].name, cmdMap[i].minmatch ) ) return( cmdMap[i].ID ); } return( cmd_MAX ); } /* getCmdID */ static sessCmdID getSessCmdID( const char *name ) /* ------------------------------------------------------------------------ ** * Convert a session command name string into a sessCmdID value. * * Input: name - A pointer to the session command name to convert. * * Output: The matching sessCmdID value, or scmd_MAX if there was no * match. * * ------------------------------------------------------------------------ ** */ { int i; for( i = 0; (NULL != sessCmdMap[i].name); i++ ) { if( prefixMatch( name, sessCmdMap[i].name, sessCmdMap[i].minmatch ) ) return( sessCmdMap[i].ID ); } return( scmd_MAX ); } /* getSessCmdID */ static void cmdHelp( const char *cmdstr ) /* ------------------------------------------------------------------------ ** * Produce help for the specified command. * * Input: cmdstr - A pointer to a string containing the command for * which help was requested. * * Output: * * ------------------------------------------------------------------------ ** */ { const char **s; int i; cmdID cID = getCmdID( cmdstr ); if( cID >= cmd_MAX ) { Err( "%s is not familiar with \"%s\".\n", progName, cmdstr ); return; } s = cmdhelp_Msg[cID]; for( i = 0; (s[i] != NULL); i++ ) { Say( "%s\n", s[i] ); } } /* cmdHelp */ static void sessCmdHelp( const char *scmdstr ) /* ------------------------------------------------------------------------ ** * Produce help for the specified session subcommand. * * Input: cmdstr - A pointer to a string containing the subcommand for * which help was requested. * * Output: * * ------------------------------------------------------------------------ ** */ { const char **s; int i; cmdID scID = getSessCmdID( scmdstr ); if( scID >= scmd_MAX ) { Err( "%s is not familiar with the \"%s\" session subcommand.\n", progName, scmdstr ); return; } s = scmdhelp_Msg[scID]; for( i = 0; (s[i] != NULL); i++ ) { Say( "%s\n", s[i] ); } } /* sessCmdHelp */ static void sessCmdVerb( const char *verbVal ) /* ------------------------------------------------------------------------ ** * Set or adjust the verbosity level. * * Input: cmdstr - A pointer to a string containing the new verbosity * level, or adjustment value. * * Output: * * ------------------------------------------------------------------------ ** */ { int newval; switch( *verbVal ) { case '-': case '+': newval = Verbose + strtol( verbVal, NULL, 10 ); break; default: newval = strtol( verbVal, NULL, 10 ); } if( newval < 0 ) Verbose = 0; else { if( newval > 255 ) Verbose = 255; else Verbose = newval; } } /* sessCmdVerb */ static void sessCmdFrag( stib_Ctx *Ctx, Gbfr *rb, off_t *offset, int readsize, off_t fsize, FILE *inf ) /* ------------------------------------------------------------------------ ** * Send a fragment. * * Input: Ctx - A pointer to the connected BITS session context. * rb - A pointer to a reusable read buffer, used to read * the file contents and pass those contents to the * function that sends the fragment. * offset - File offset from which the bytes are to be read. * readsize - The number of bytes to read. The buffer in * has already been expanded to ensure that it can * hold bytes. * fsize - File size, used to prevent us attempting to read * beyond EOF. * inf - A pointer to the opened input file. * * Output: * * Notes: This function is fairly hairy. It was difficult to find a * clean place at which to separate this logic from that of * . We want a consistent buffer for reading from * the file, and we need to know things like the current offset * and file size. This would have been easier in Pascal. * * ------------------------------------------------------------------------ ** */ { stib_parsedMsg *pMsg; int status = 200; /* Initialize the read buffer. * If we were specifically given a read size of zero (0), * then don't actually read anything. */ clearGbfr( rb ); if( readsize > 0 ) { rb->len = (int)fread( rb->bufr, 1, readsize, inf ); if( ferror( inf ) && !feof( inf ) ) { status = -1; Err( "Error reading from input; %s.\n", ErrStr ); } } /* If the read was successful (or if it was skipped), * then send the fragment to the server. */ if( status > 0 ) { if( vLvl(1) ) Err( "Sending %ld-%ld/%ld.\n", *offset, (*offset + rb->len) - 1, fsize ); pMsg = stib_Fragment( Ctx, *offset, lenGbfr( rb ), bfrGbfr( rb ) ); if( NULL == pMsg ) { status = -1; Err( "Send Fragment failed: %s.\n", stib_errStr( stib_ctxErr( Ctx ) ) ); } } /* If the read was successful (or skipped) and the fragment was sent * successfully (indicated by the server sending a response), then * dump the response. */ if( status > 0 ) { dump_pMsg( pMsg ); status = pMsg->status; stib_freepMsg( pMsg ); } /* If the server response indicated success, * then update the value of <*offset> and return. */ if( status > 0 ) { *offset += lenGbfr( rb ); return; } /* If any of the above steps failed, reset the current file position * to the initial offset so that we can (perhaps) try again. */ (void)fseeko( inf, *offset, SEEK_SET ); } /* sessCmdFrag */ static int sessCmdClose( stib_Ctx *Ctx ) /* ------------------------------------------------------------------------ ** * Close a completed BITS file upload. * * Input: Ctx - A pointer to the connected BITS session context. * * Output: The status code returned by the server, as an integer, or * -1 if there was an error communicating with the server and, * therefore, no status was received. * * ------------------------------------------------------------------------ ** */ { stib_parsedMsg *pMsg = stib_CloseSession( Ctx ); int status = -1; if( NULL == pMsg ) { Err( "Unable to close the BITS session; %s.\n", stib_errStr( stib_ctxErr( Ctx ) ) ); } else { status = pMsg->status; dump_pMsg( pMsg ); stib_freepMsg( pMsg ); } return( status ); } /* sessCmdClose */ static int sessCmdCancel( stib_Ctx *Ctx ) /* ------------------------------------------------------------------------ ** * Cancel an in-progress BITS file upload. * * Input: Ctx - A pointer to the connected BITS session context. * * Output: The status code returned by the server, as an integer, or * -1 if there was an error communicating with the server and, * therefore, no status was received. * * ------------------------------------------------------------------------ ** */ { stib_parsedMsg *pMsg = stib_CancelSession( Ctx ); int status = -1; if( NULL == pMsg ) { Err( "Unable to cancel the BITS session; %s.\n", stib_errStr( stib_ctxErr( Ctx ) ) ); } else { status = pMsg->status; dump_pMsg( pMsg ); stib_freepMsg( pMsg ); } return( status ); } /* sessCmdCancel */ static void sessCmdQuit( stib_Ctx *Ctx, const bool Cancel ) /* ------------------------------------------------------------------------ ** * Cancel or close a transfer, depending upon the value of . * * Input: Ctx - A pointer to the connected BITS session context. * Cancel - Boolean; if true then a BITS Cancel message will be * sent to the server, otherwise a BITS Close message * is sent. * * Output: * * ------------------------------------------------------------------------ ** */ { stib_parsedMsg *pMsg; if( Cancel ) { if( vLvl(1) ) Err( "Canceling the unfinished transfer session.\n" ); pMsg = stib_CancelSession( Ctx ); } else { if( vLvl(1) ) Err( "Closing the completed session.\n" ); pMsg = stib_CloseSession( Ctx ); } if( NULL == pMsg ) { Err( "%s failed: %s.\n", (Cancel ? "Cancel" : "Close"), stib_errStr( stib_ctxErr( Ctx ) ) ); return; } dump_pMsg( pMsg ); stib_freepMsg( pMsg ); } /* sessCmdQuit */ static void runSession( FILE *inf, off_t fsize, stib_Ctx *Ctx ) /* ------------------------------------------------------------------------ ** * Run a BITS session interactively. * * Input: inf - The Input file, from which blocks of data are read. * fsize - Size, in bytes, of the file being sent. * Ctx - A pointer to the connected BITS session context. * * Output: * * Fix: This function, , and should all be * reviewed for redundant code. * * ------------------------------------------------------------------------ ** */ { char *line; int count; char **array; sessCmdID scmd; off_t tmp_off; long int tmpl; bool done = false; off_t offset = 0; int readsize = bSIZE; Gbfr *ReadBfr = allocGbfr( bSIZE ); if( !ReadBfr ) { Err( "Memory allocation failure creating a read buffer.\n" ); return; } Err( "STiBtest!\n%s\nIf in doubt, type 'help'.\n", Copyright ); while( !done ) { line = readline( "STiBtest> " ); if( !isEmpty( line ) ) { add_history( line ); count = 0; array = parseCmdLine( line, &count ); if( array ) { scmd = getSessCmdID( array[0] ); switch( scmd ) { case scmd_Verbosity: case scmd_Fragment: tmpl = 2; break; case scmd_Help: tmpl = 2; break; default: tmpl = 1; break; } if( (count > tmpl) && vLvl(1) ) Err( "Excess command parameters ignored.\n" ); switch( scmd ) { case scmd_Help: sessCmdHelp( array[(count > 1 ? 1 : 0)] ); break; case scmd_Verbosity: if( count > 1 ) sessCmdVerb( array[1] ); Err( "Verbosity level: %d.\n", Verbose ); break; case scmd_Fragment: if( count > 1 ) { if( (tmpl = strtol( array[(count > 2) ? 2 : 1], NULL, 0 )) < 0 ) { Err( "Invalid fragment size (%ld); command ignored.\n", tmpl ); break; } else readsize = (int)tmpl; } if( count > 2 ) { if( (tmp_off = newOffset( array[1], fsize, inf )) < 0 ) { Err( "Offset out of range (%ld); command ignored.\n", tmp_off ); break; } else offset = tmp_off; } if( growGbfr( ReadBfr, readsize ) < 0 ) Fail( "Internal memory allocation failure.\n" ); sessCmdFrag( Ctx, ReadBfr, &offset, readsize, fsize, inf ); break; case scmd_Disconnect: stib_Disconnect( Ctx ); break; case scmd_Cancel: done = is2xx( sessCmdCancel( Ctx ) ) ? true : false; break; case scmd_Close: done = is2xx( sessCmdClose( Ctx ) ) ? true : false; break; case scmd_Quit: sessCmdQuit( Ctx, (feof( inf ) ? false : true) ); done = true; break; default: Err( "Unknown command: \"%s\".\n", array[0] ); break; } free( array ); } free( line ); } } freeGbfr( ReadBfr ); } /* runSession */ static void cmdPing( const char *url ) /* ------------------------------------------------------------------------ ** * Open a connection and send a BITS Ping command to a server. * * Input: url - The URI that identifies the target of the ping request. * * Output: * * ------------------------------------------------------------------------ ** */ { stib_Ctx *Ctx; stib_parsedURI *pUri; stib_parsedMsg *pMsg; stib_Error stiberr[1]; const char *portname; const char *pathname; /* Parse the URL string. */ pUri = stib_parseURI( url ); if( NULL == pUri ) { Err( "Unable to parse the URI string: \"%s\".\n", url ); return; } /* Use default values if none are given. */ portname = ifNotThen( pUri->port, (pUri->secure)?DefaultSPort:DefaultPort ); pathname = ifNotThen( pUri->path, DefaultPath ); /* Diagnostics. */ if( vLvl(3) ) { Err( "Preparing to send a " ); if( pUri->secure ) Err( "secure " ); Err( "BITS Ping to host %s:%s.\n", pUri->serv, portname ); } /* Create the TCP connection. */ Ctx = stib_Connect( pUri->serv, portname, stiberr ); if( !Ctx ) { Err( "Unable to connect to %s; %s.\n", pUri->serv, stib_errStr( stiberr ) ); free( pUri ); return; } /* Now do the ping. */ pMsg = stib_Ping( Ctx, pathname, Agent ); if( NULL == pMsg ) Err( "Unable to ping %s; %s.\n", pUri->serv, stib_errStr( stib_ctxErr( Ctx ) ) ); else { dump_pMsg( pMsg ); stib_freepMsg( pMsg ); } /* Cleanup & go home. */ (void)stib_CloseCtx( Ctx ); free( pUri ); } /* cmdPing */ static void cmdSend( const char *fname, const char *url ) /* ------------------------------------------------------------------------ ** * Create a session, send fragments, and close the session. * * Input: fname - The name of the file to be transferred. * url - Destination URL. * * Output: * * ------------------------------------------------------------------------ ** */ { FILE *inf; char bufr[bSIZE]; off_t offset; size_t len; stib_parsedURI *pUri; stib_Ctx *Ctx; stib_Error err[1]; stib_parsedMsg *pMsg; int result; const char *portname; const char *pathname; struct stat statbuf[1]; if( stat( fname, statbuf ) ) { Err( "Could not determine the file size of file \"%s\"; %s.\n", fname, ErrStr ); return; } /* Parse the URL. */ pUri = stib_parseURI( url ); if( NULL == pUri ) { Err( "Could not parse \"%s\" to discover the server name or IP.\n", url ); return; } /* Use default values if none are given. */ portname = ifNotThen( pUri->port, (pUri->secure)?DefaultSPort:DefaultPort ); pathname = ifNotThen( pUri->path, DefaultPath ); /* Open the input file. */ inf = fopen( fname, "r" ); if( NULL == inf ) { Err( "Cannot open file \"%s\"; %s.\n", fname, ErrStr ); free( pUri ); return; } /* Connect to the Server. */ Ctx = stib_Connect( pUri->serv, portname, err ); if( NULL == Ctx ) { Err( "Unable to connect to %s; %s.\n", pUri->serv, stib_errStr( err ) ); free( pUri ); (void)fclose( inf ); return; } if( vLvl( 3 ) ) Err( "Connected to http%s://%s/.\n", pUri->secure?"s":"", pUri->serv ); /* Create a BITS session. */ if( vLvl(3) ) Err( "Creating the BITS session.\n" ); pMsg = stib_CreateSession( Ctx, pathname, Agent, statbuf->st_size ); if( NULL == pMsg ) { Err( "Unable to create a BITS session with %s; %s.\n", pUri->serv, stib_errStr( stib_ctxErr( Ctx ) ) ); free( pUri ); (void)fclose( inf ); (void)stib_CloseCtx( Ctx ); return; } dump_pMsg( pMsg ); if( !is2xx( pMsg->status ) ) { free( pUri ); (void)fclose( inf ); (void)stib_CloseCtx( Ctx ); stib_freepMsg( pMsg ); return; } stib_freepMsg( pMsg ); /* Read and upload fragments. */ if( vLvl(3) ) Err( "Sending fragments.\n" ); offset = 0; len = fread( bufr, 1, bSIZE, inf ); result = 200; while( (len > 0) && is2xx( result ) ) { pMsg = stib_Fragment( Ctx, offset, len, bufr ); if( NULL == pMsg ) { Err( "Unable to close the BITS session with %s; %s.\n", pUri->serv, stib_errStr( stib_ctxErr( Ctx ) ) ); free( pUri ); (void)fclose( inf ); (void)stib_CloseCtx( Ctx ); return; } result = pMsg->status; dump_pMsg( pMsg ); stib_freepMsg( pMsg ); offset += len; len = fread( bufr, 1, bSIZE, inf ); } /* Close the BITS session. */ if( vLvl(3) ) Err( "Closing the BITS session.\n" ); pMsg = stib_CloseSession( Ctx ); if( NULL == pMsg ) { Err( "Unable to close the BITS session with %s; %s.\n", pUri->serv, stib_errStr( stib_ctxErr( Ctx ) ) ); free( pUri ); (void)fclose( inf ); (void)stib_CloseCtx( Ctx ); return; } dump_pMsg( pMsg ); stib_freepMsg( pMsg ); /* Clean up. */ free( pUri ); (void)fclose( inf ); result = stib_CloseCtx( Ctx ); if( result ) { Err( "Could not close the connection to the server; %s.\n", strerror( result ) ); } } /* cmdSend */ static void cmdGet( const char *url, const char *fname, bool BCE, off_t off, uint len ) /* ------------------------------------------------------------------------ ** * Copy a file from a remote location using the HTTP GET command. * * Input: url - The URL of the source file to copy. * fname - The pathname of the destination file. * BCE - BranchCache Encoding. If enabled, the GET command * will include a request for peerdist Content * Information, which the server will only send if it * is available. * off - For range requests, the starting offset. Zero means * the start of the content (of course). * len - For range requests, the number of bytes being * requested. Zero means "all of them". * * Output: * * ------------------------------------------------------------------------ ** */ { stib_parsedURI *pUri; const char *portname; const char *pathname; FILE *outF; stib_Ctx *Ctx; stib_Error err[1]; stib_parsedMsg *pMsg; int result; unsigned char bufr[bSIZE]; ssize_t readlen; /* Parse the URL. */ pUri = stib_parseURI( url ); if( NULL == pUri ) { Err( "Could not parse \"%s\" to discover the server name or IP.\n", url ); return; } /* Use default values if none are given. */ portname = ifNotThen( pUri->port, (pUri->secure)?DefaultSPort:DefaultPort ); pathname = ifNotThen( pUri->path, DefaultPath ); /* Connect to the Server. */ Ctx = stib_Connect( pUri->serv, portname, err ); if( NULL == Ctx ) { Err( "Unable to connect to %s; %s.\n", pUri->serv, stib_errStr( err ) ); free( pUri ); return; } if( vLvl( 3 ) ) Err( "Connected to http%s://%s/.\n", pUri->secure?"s":"", pUri->serv ); /* Open the output file. */ outF = fopen( fname, "w" ); if( NULL == outF ) { Err( "Cannot create file \"%s\"; %s.\n", fname, ErrStr ); free( pUri ); (void)stib_CloseCtx( Ctx ); return; } /* Send the get request, and read the response headers. */ pMsg = stib_Get( Ctx, pathname, Agent, off, len, BCE ); if( NULL == pMsg ) { Err( "Unable to create an HTTP%s session with %s; %s.\n", (pUri->secure) ? "s" : "", pUri->serv, stib_errStr( stib_ctxErr( Ctx ) ) ); free( pUri ); (void)fclose( outF ); (void)stib_CloseCtx( Ctx ); return; } dump_pMsg( pMsg ); /* Copy the payload to the output file. */ readlen = stib_readBody( Ctx, bufr, bSIZE ); while( readlen > 0 ) { (void)fwrite( bufr, 1, readlen, outF ); readlen = stib_readBody( Ctx, bufr, bSIZE ); } /* Clean up. */ stib_freepMsg( pMsg ); free( pUri ); (void)fclose( outF ); result = stib_CloseCtx( Ctx ); if( result ) Err( "Could not close the connection to the server; %s.\n", strerror( result ) ); } /* cmdGet */ static void cmdSess( const char *fname, const char *url ) /* ------------------------------------------------------------------------ ** * Create a session, and put the user in control of the upload. * * Input: fname - The name of the file to be transferred. * url - Destination URL. * * Output: * * Notes: This starts off identically to , but once the * connection has been established it switches into interactive * mode, allowing the user to control the sending of fragments * and close or cancel messages. The user may also disconnect * the session without losing local state. * * ------------------------------------------------------------------------ ** */ { struct stat statbuf[1]; stib_parsedURI *pUri; const char *portname; const char *pathname; FILE *inf; stib_Error err[1]; stib_Ctx *Ctx; stib_parsedMsg *pMsg; int result; if( stat( fname, statbuf ) ) { Err( "Could not determine the file size of file \"%s\"; %s.\n", fname, ErrStr ); return; } /* Parse the URL. */ pUri = stib_parseURI( url ); if( NULL == pUri ) { Err( "Could not parse \"%s\" to discover the server name or IP.\n", url ); return; } /* Use default values if none are given. */ portname = ifNotThen( pUri->port, (pUri->secure)?DefaultSPort:DefaultPort ); pathname = ifNotThen( pUri->path, DefaultPath ); /* Open the input file. */ inf = fopen( fname, "r" ); if( NULL == inf ) { Err( "Cannot open file \"%s\"; %s.\n", fname, ErrStr ); free( pUri ); return; } /* Connect to the Server. */ Ctx = stib_Connect( pUri->serv, portname, err ); if( NULL == Ctx ) { Err( "Unable to connect to %s; %s.\n", pUri->serv, stib_errStr( err ) ); free( pUri ); (void)fclose( inf ); return; } if( vLvl( 3 ) ) Err( "Connected to http%s://%s/.\n", pUri->secure?"s":"", pUri->serv ); /* Create a BITS session. */ if( vLvl(3) ) Err( "Creating the BITS session.\n" ); pMsg = stib_CreateSession( Ctx, pathname, Agent, statbuf->st_size ); if( NULL == pMsg ) { Err( "Unable to create a BITS session with %s; %s.\n", pUri->serv, stib_errStr( stib_ctxErr( Ctx ) ) ); free( pUri ); (void)fclose( inf ); (void)stib_CloseCtx( Ctx ); return; } result = pMsg->status; dump_pMsg( pMsg ); stib_freepMsg( pMsg ); /* Was the session successfully created? * See http://msdn.microsoft.com/en-us/library/aa362771.aspx * Which suggests (but does not state) that only 200 or 201 are truly * successful responses. */ if( !is2xx( result ) ) { Err( "The server did not accept the Create-Session request.\n" ); free( pUri ); (void)fclose( inf ); (void)stib_CloseCtx( Ctx ); return; } /* Okay... we have established a session with the server and can send * fragments, or cancel the session, or whatever. Put control of this * into the user's hands. */ runSession( inf, statbuf->st_size, Ctx ); free( pUri ); (void)fclose( inf ); (void)stib_CloseCtx( Ctx ); } /* cmdSess */ static void parseGetCmd( const int cargc, char *const cargv[] ) /* ------------------------------------------------------------------------ ** * Parse a "get" command line. * * Input: cargc - Command argument count, including the command. * cargv - Command argument vector. An array of pointers to the * strings read from the command-line (a la in * . * * Notes: The "get" command line has gotten complicated. * * ------------------------------------------------------------------------ ** */ { char *url = cargv[cargc - 2]; char *filename = cargv[cargc - 1]; bool peerdist = false; char *colpos = NULL; off_t off = 0; uint len = 0; int max = cargc - 2; int i; i = 1; while( i < max ) { if( prefixMatch( cargv[i], "peerdist", 1 ) ) peerdist = true; else { /* Attempt to find and parse a "range offset:length" option. */ if( prefixMatch( cargv[i], "range", 1 ) ) { i++; colpos = strchr( cargv[i], ':' ); if( NULL == colpos ) Fail( "A range specifier requires a colon (':'); \"%s %s\".\n", cargv[i-1], cargv[i] ); off = (off_t)strtoul( cargv[i], NULL, 0 ); len = (uint)strtoul( colpos+1, NULL, 0 ); } } i++; } cmdGet( url, filename, peerdist, off, len ); } /* parseGetCmd */ static void runCmd( const int cargc, char *const cargv[] ) /* ------------------------------------------------------------------------ ** * Execute a STiBtest command. * * Input: cargc - Command argument count, including the command. * cargv - Command argument vector. An array of pointers to the * strings read from the command-line (a la in * . * * Output: * * ------------------------------------------------------------------------ ** */ { switch( getCmdID( cargv[0] ) ) { case cmd_Help: { if( cargc <= 1 ) Usage( (vLvl(2) ? verboseMsg : helpMsg), progName ); else cmdHelp( cargv[1] ); break; } case cmd_Ping: { if( cargc != 2 ) cmdHelp( cargv[0] ); else cmdPing( cargv[1] ); break; } case cmd_Send: if( cargc != 3 ) cmdHelp( cargv[0] ); else cmdSend( cargv[1], cargv[2] ); break; case cmd_Get: if( cargc > 2 ) parseGetCmd( cargc, cargv ); else cmdHelp( cargv[0] ); break; case cmd_Sess: if( cargc != 3 ) cmdHelp( cargv[0] ); else cmdSess( cargv[1], cargv[2] ); break; default: Err( "%s doesn't know command \"%s\". Try \"%s help\"\n", progName, cargv[0], progName ); break; } } /* runCmd */ static void readCmdLine( int argc, char *argv[] ) /* ------------------------------------------------------------------------ ** * Read command line options and set global settings. * * Input: argc - You know what this is. * argv - You know what to do. * * Output: none * * ------------------------------------------------------------------------ ** */ { int c; bool SpewHelp = false; bool SpewVersion = false; opterr = 0; /* We'll handle option errors on our own. */ while( (c = getopt( argc, argv, "hqvV" )) >= 0 ) { switch( c ) { case 'h': SpewHelp = true; break; case 'q': Verbose = 0; break; case 'v': Verbose++; break; case 'V': SpewVersion = true; break; default: Err( "Error: Unrecognized option on command line: '-%c'\n\n", optopt ); Usage( helpMsg, argv[0] ); exit( EXIT_FAILURE ); } } /* Print version information, * then exit(3) unless help was requested. */ if( SpewVersion ) { if( vLvl( 2 ) ) { Say( "%s\n", ID ); if( vLvl( 3 ) ) Say( "%s\n%s\n", Copyright, License ); } else Say( "%s\n", Revision ); if( !SpewHelp ) exit( EXIT_SUCCESS ); } /* Spew the help message, and exit the program. */ if( SpewHelp ) { Usage( (vLvl(2) ? verboseMsg : helpMsg), argv[0] ); exit( EXIT_SUCCESS ); } /* Copy the program name in case we need it later. */ if( NULL == (progName = strdup( argv[0] )) ) Fail( "Memory allocation failure calling strdup(3); %s.\n", ErrStr ); /* There should be a command left on the command line. * If so, run it. If not, spew the help message. */ if( optind < argc ) { runCmd( (argc - optind), &(argv[optind]) ); } else { Err( "No %s command specified.\n\n", progName ); Usage( (vLvl(2) ? verboseMsg : helpMsg), progName ); } } /* readCmdLine */ /* -------------------------------------------------------------------------- ** * Mainline: */ int main( int argc, char* argv[] ) /* ------------------------------------------------------------------------ ** * Program Mainline. * * Input: argc - You know what this is. * argv - You know what to do. * * Output: EXIT_FAILURE on failure, else EXIT_SUCCESS. * * ------------------------------------------------------------------------ ** */ { readCmdLine( argc, argv ); /* Read and execute the command line. */ return( EXIT_SUCCESS ); /* All done. */ } /* main */ /* ========================================================================== */