Skip to content
/ qck Public

HTTP request client & server used to test proxy services

License

Notifications You must be signed in to change notification settings

G5unit/qck

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

qck

Qck is HTTP test request/response generator with ability to act as both HTTP client and server, useful when testing behaviour of proxy servers and load balancers. Qck is written in Node.js with aim to be used in a dev/testing environment. When contents of file are being sent as HTTP body, crude transmit rate controls are in place allowing one to simulate HTTP Layer 7 data rate speeds of a few Bytes per second to MBytes per second, or test behaviour of long lasting connections. Ability to be HTTP Client and Server in one app enables qck to match requests to responses and correlate validation.

Single configuration file is used to pass all settings so saving the test setup could be as simple as saving one config file.


Usage

qck.js <configuration file>

Example:

 qck.js sample_qck.config

Qck will read configuration file specified and do several things.

  • Look for serverresponse objects and start a HTTP server to respond to requests received matching configured method and pathname. Listener IP and port are configured under __globalconfig__ object.
  • Use clientrequest objects to setup tests available to be run. Tests are HTTP requests sent to server and serverPort under __globalconfig__ .

Running tests, that is generating HTTP requests, is independent of listener HTTP server side of the application. One could run tests towards external server, and at the same time have the listener respond to requests generated by external clients.

Using sample configuration documented further in this document, file output from '--list' interactive mode option is:

Tests available to run:
  test1
  test2
  test3
  test4
Listener setup:
  Defined under test:  test1 	Method::Path =   PUT::/
  Defined under test:  test3 	Method::Path =   PUT::/test3
  Defined under test:  test4 	Method::Path =   GET::/test4

To run test1 type test1 and press enter. Sample output:

test1
Test: test1 started   Sun Feb 22 2015 17:55:07 GMT-0700 (MST)
Test: test1, Status: Client check Passed , Server check Passed  Sun Feb 22 2015 17:55:37 GMT-0700 (MST)

To run multiple tests specify them as a comma separated list. Each request sent is tagged with unique id in form of custom HTTP header 'x-qck-id' whose value is random generated. This is used to match request sent to one received by listener and match serverresponse to particular test run instance of clientrequest.

test1,test2,test2
Test: test1 started   Sun Feb 22 2015 18:00:34 GMT-0700 (MST)
Test: test2 started   Sun Feb 22 2015 18:00:34 GMT-0700 (MST)
Test: test2 started   Sun Feb 22 2015 18:00:34 GMT-0700 (MST)
Test: test2, Status: Client check PassedSun Feb 22 2015 18:00:34 GMT-0700 (MST)
Test: test2, Status: Client check PassedSun Feb 22 2015 18:00:34 GMT-0700 (MST)
Test: test1, Status: Client check PassedSun Feb 22 2015 18:00:44 GMT-0700 (MST) , Server check Passed  Sun Feb 22 2015 18:00:44 GMT-0700 (MST)

###Interactive mode If 'runtests' option is set in configuration file, qck will run tests listed, and exit upon completion. If no 'runtests' option is set in configuration file, qck enters interactive mode where you have these options available:

Options

--file=<filename>       Set file to log output to
--nofile                Disables file logging, if file is being logged to it is closed.
--log=[0-5]             Set log level
--list                  Lists available tests to run, and Listener method::path 
--exit                  Exits qck
<testname>[,<testname>] Comma separated list of tests to run. All tests are 
                        ran simultaneously (in parallel), or if using
						interactive mode at time of command execution.

Logging

Logging applies to messages that report test results, and data related to HTTP requests and responses. App errors are always outputted to console (stdout). --file or filename under __globalconfig__ in configuration file specify where test related messages are written to. If this file already exists, it is overwritten.

Log Levels

0 - All messages suppressed
1 - Outputs test result (one per line),
2 - Level 1 messages  + servercheck result for requests that do not have qck-ID
    header (not generated by qck or header was stripped in transit). These are
    reported as 'Unknown Client'.
3 - Level 2 messages + Listener side bad requests received, requests for 
    pathname and method not defined in cfg file under "serverresponse" object.
4 - Level 2 messages + request and response HTTP objects with basic attributes
    (url, method, statusCode, headers, body)
5 - Level 2 messages + full Node.js HTTP request and response objects

Configuration file

Configuration file is in JSON format. Currently there is no validation checking of this file, other then JSON.parse() exception being caught if present.

####Global App settings

__globalconfig__ object is used for global application settings. It can have these options:

  • listenerIp - IP address to bind the HTTP server listener to, default is 127.0.0.1
  • listenerPort - TCP port to bind HTTP server listener to, default is 6589
  • server - Hostname, domain name, or IP address where to send HTTP requests, default is 127.0.0.1
  • serverPort - TCP port of server, default is 6589
  • loglevel - Log level to start with, default is 1
  • outputfile - File to log test data.
  • runtests - Comma separated list of tests to execute, disables qck interactive mode.

####Test settings

Configuring test options consists of clientrequest, clientcheck, serverresponse, and servercheck objects. If configuration file contains multiple tests with the same name, the last one read in will be used. Same goes for case where there are multiple tests, with different names, yet have the same serverresponse pathname and method. Last serverresponse object read in will be used for Listener side setups.

#####clientrequest

Clientrequest object is used to configure parameters for HTTP request to be sent when test is ran. Options specified here are passed to Node.js http.new.request method. See Node.js HTTP ClientRequest for valid options. Notable ones are:

If no hostname and port option is given here, server and serverPort from __globalconfig__ will be applied.

In addition, following qck only options are supported:

  • filename - file to read data from and send as part of HTTP body

  • transrate - transmission rate in bytes per second to send HTTP body data at

  • transfreq - frequency in milliseconds at which to send packets when transrate is used

      "test2":                
        { "clientrequest":  
                       { "pathname": "/test2",
                         "method": "POST",
                         "filename": "file.sample",
      			        "transrate": "300000",
      					"transfreq": "25"
                       }
    

If you specify a file whose contents are to be sent, using filename option, file is sent as multipart binary data in HTTP body, with boundary markers. boundaryKey is a random generated 16 digit number. Header added to HTTP request or response:

Content-Type :'multipart/form-data; boundary="+boundaryKey+"'

Written to HTTP body:

'--'+boundaryKey+'\r\n'
'Content-Type: application/octet-stream\r\n' 
'Content-Disposition: form-data; name="filename"; filename="filename"\r\n'
'Content-Transfer-Encoding: binary\r\n\r\n'

<contents of file as raw binary data>

'\r\n--'+boundaryKey+'--'

By default (transrate and transfreq options not specified) contents of file are transmitted as fast as possible. Underlying Node.js fs.readStream is piped to HTTP request or response writeStream.

When using filename, transrate and transfreq are there to control transmission rate, and both options must be specified. Data throttling is done on Layer7 and does not account for any network stack layer 2-6 data present in each packet, such as Layer2, IP, TCP headers.

  • transrate is used to specify transmission rate in Bytes per Second. Minimum value is 1, max is ... more then what (max_chunk[Bytes] * transfreq / 1000[ms]) can support. max_chunk is 65536.
  • transfreq is used to specify at what interval should data be written to the network stack (for Node.js this is stream). This interval is in milliseconds, with minimum value of 1, max is not limited.

Using these two parameters amount of data (chunk) needed to be transmitted each transfreq period of time to achieve transrate is calculated and written to HTTP request or response Node.js writableStream. Note that max of this chunk size is 65536 bytes. Setting transfreq to 1ms does not produce good results due everything node.js stack has to do in such a small period of time, if configured server side (listener) processes all the data sent by requesting. Good rule of thumb, set transfreq to as high a value as possible to achieve desired transmission rate.

Setting transrate and transfreq examples:

Setting "transfreq" to 20, and "transrate" to 70000000 will result in qck writing 65536 Bytes (in Node.js to writableStream) of data every 20ms producing an approximate transmission rate of 3276800 Bytes per second, or approximately 26mbits per second. To achieve desired transrate at 20ms frequency, chunk size would have to be 1400000 Bytes in size. This is more then coded max of 65536 and is automatically throttled to this max setting.
Setting "transfreq" to 30, and "transrate" to 8000 will result in qck writing 240 Bytes of data every 30ms producing an approximate transmission rate of 8000 Bytes per second, or approximately 15kbits per second. 	

One could set transrate to 10 and transfreq to 100 resulting in one byte of payload sent every 100 milliseconds. Sending anything less then 64 Bytes of payload data probably means that more data is transmitted as overhead (other stack layers) then actual payload.

If size of payload (chunk) sent every transfreq is larger then what your network driver can put on the wire, you will see multiple packets on the wire per data chunk.

#####clientcheck

Clientcheck object is used to set parameters in HTTP response received to be crossreferenced. This is a simple one for one comparrison of Node.js HTTP response object properties. In following example status code (Node.js HTTP response object "statusCode" property) is checked to see if it equals 200.

        "clientcheck":  
                   { "statusCode": "200"
                   },

#####serverresponse

Serverresponse object is used to set parameters for listener side. Supported options are any option supported by Node.js HTTP server response object. Internal to qck app, settings here are just copied over to that object.

method and pathname are used to match HTTP requests received.

In following example any request received with method GET wuth url path /test1 will be responded to with status Code 200, and include body "Test1 response here".

       "serverresponse":  
                      { "pathname": "/test1",
                        "method": "GET",
                        "statusCode": "200",
                        "body": "Test 1 response here"
                      }

#####servercheck

HTTP requests received by listener that match serverresponse will be checked in same manner clientcheck works. Failing a check does not prevent the response from being sent. Test result will have count of mistmatches in check.

In following example requests received will be crossreferenced to see if they contain a header x-qck-id with value of 123456789.

       "servercheck":   
                   { "headers": {"x-qck-id":"123456789"}
                   }

####Sample Configuration File

{ 
 "__globalconfig__":						
                 { "listenerIp":"127.0.0.1",
                   "listenerPort":"6589",
                   "server":"127.0.0.1",
                   "serverPort":"6589",
                   "loglevel":"1"
                  },
 "test1":                
      { "clientrequest":  
                     { "pathname": "/test1",
                       "method": "GET",
                     },
        "clientcheck":  
                   { "statusCode": "200"
                   },
        "serverresponse":  
                      { "pathname": "/test1",
                        "method": "GET",
                        "statusCode": "200",
                        "body": "Test 1 response here"
                      }
         },
 "test2":                
      { "clientrequest":  
                     { "pathname": "/testt2",
                       "method": "POST",
                       "filename": "file.sample",
				        "transrate": "300000",
						"transfreq": "25"
                     },
        "clientcheck":  
                   { "statusCode": "200"
                   },
        "serverresponse":  
                      { "pathname": "/test2",
                        "method": "POST",
                        "statusCode": "200"
                      },
        "servercheck":   
                   { "method": "GET"
                   }
       },
 "test3":                
      { "clientrequest":  
                     { "pathname": "/test3",
                       "method": "GET",
                     },
        "clientcheck":  
                   { "statusCode": "302"
                   },
        "serverresponse":  
                      { "pathname": "/test3",
                        "method": "GET",
                        "statusCode": "302",
                        "headers": {"Contact":"/test4"},
                        "body": "Test 3 response here"
                      },
        "servercheck":   
                   { "headers": {"x-qck-id":"123456789"}
                   }
       },
 "test4":                
      { "serverresponse":  
                      { "pathname": "/test4",
                        "method": "GET",
                        "statusCode": "200",
                        "body": "Test 4 response here"
                      }
      }
                  
}

###Connection throttling internals

Throttling transmission rates inside Qck are implemented in such manner:

  • Buffer (Array) containing 20 transmission chunks is populated at 1ms intervals, one chunk at a time. Once Buffer is full, this Node.js function will schedule it self to populate one chunk every "transfreq"*0.8 milliseconds. Node.js fs.readableStream used to get data from file is paused until available data could be entered into the Buffer.
  • Simultaneously, function to send one chunk at a time from Buffer, every "transfreq" milliseconds is started using Node.js setInterval() function.
  • Once all data is read from file, entered into the Buffer, and transmitted (Buffer is empty) closing boundaryKey is transmitted.

###Areas for improvement

  • Function and logic that enters data into Buffer could use optimizing.
  • Buffer size could be calculated based on transfreq and size of chunk to transmit, so instead of populating one chunk at a time, we populate multiple chunks at a time, and reschedule function to populate Buffer at X times larger interval then "timefreq", reducing load on event stack. This requires setting fs.readableStream internal buffer size to match X number of chunks. This is where 65536 chunk size comes from. On my test system (Mac OS X 64 bit), with default settings, Node.js fs.on('data', fn(chunk) { ...}) returns chunk size of 65536 bytes.
  • fs.readableStream internal buffer size setting should be optimized, right now using defaults
  • HTTP writableStream internal buffer could be optimized to match chunk size.

About

HTTP request client & server used to test proxy services

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published